From 3beeff1c820273f1ae001238bc9b12fbfc1d1407 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Tue, 6 Jul 2021 12:13:53 +0100 Subject: [PATCH 01/47] Use target=_blank on MissingAnnotation Read More link I would like Backstage to stay open so I can continue to work with it while I read the docs. Signed-off-by: David Tuite --- .../src/components/EmptyState/MissingAnnotationEmptyState.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx index 39cce709c0..2b2f2b7738 100644 --- a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx +++ b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx @@ -76,6 +76,8 @@ export const MissingAnnotationEmptyState = ({ annotation }: Props) => { variant="contained" color="primary" href="https://backstage.io/docs/features/software-catalog/well-known-annotations" + target="_blank" + rel="noopener noreferrer" > Read more From 10113f47b221ff44c36364700effb60fddecd5bc Mon Sep 17 00:00:00 2001 From: David Tuite Date: Tue, 6 Jul 2021 14:05:49 +0100 Subject: [PATCH 02/47] Use Link from core-components Signed-off-by: David Tuite --- .../components/EmptyState/MissingAnnotationEmptyState.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx index 2b2f2b7738..d477494964 100644 --- a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx +++ b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { Button, makeStyles, Typography } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; +import { Link } from '../Link'; import { EmptyState } from './EmptyState'; import { CodeSnippet } from '../CodeSnippet'; @@ -73,11 +74,9 @@ export const MissingAnnotationEmptyState = ({ annotation }: Props) => { /> From 2a13aa1b753f39d62954a7bed9f1d855cf39771c Mon Sep 17 00:00:00 2001 From: irma12 Date: Tue, 6 Jul 2021 15:06:43 +0200 Subject: [PATCH 03/47] Handle empty strings in code blocks Signed-off-by: irma12 --- .changeset/shaggy-ties-carry.md | 5 +++++ .../src/components/MarkdownContent/MarkdownContent.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/shaggy-ties-carry.md diff --git a/.changeset/shaggy-ties-carry.md b/.changeset/shaggy-ties-carry.md new file mode 100644 index 0000000000..83b7782cd0 --- /dev/null +++ b/.changeset/shaggy-ties-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Handle empty code blocks in markdown files so they don't fail rendering diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index 2167da6fd2..296df22ecd 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -65,8 +65,8 @@ type Props = { }; const renderers = { - code: ({ language, value }: { language: string; value: string }) => { - return ; + code: ({ language, value }: { language: string; value?: string }) => { + return ; }, }; From 11e66e8040004a4c79004a36854b025744b52e28 Mon Sep 17 00:00:00 2001 From: Alex Landovskis Date: Tue, 6 Jul 2021 19:55:57 -0400 Subject: [PATCH 04/47] build: bump azure-devops-node-api to 10.2.2 Signed-off-by: Alex Landovskis --- .changeset/sweet-terms-approve.md | 6 +++++ packages/backend/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- yarn.lock | 29 ++++++++++++------------- 4 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 .changeset/sweet-terms-approve.md diff --git a/.changeset/sweet-terms-approve.md b/.changeset/sweet-terms-approve.md new file mode 100644 index 0000000000..56297f694d --- /dev/null +++ b/.changeset/sweet-terms-approve.md @@ -0,0 +1,6 @@ +--- +'example-backend': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +bump azure-devops-node to 10.2.2 diff --git a/packages/backend/package.json b/packages/backend/package.json index a2c5906696..9a019c6cfe 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -48,7 +48,7 @@ "@backstage/plugin-todo-backend": "^0.1.6", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", - "azure-devops-node-api": "^10.1.1", + "azure-devops-node-api": "^10.2.2", "dockerode": "^3.2.1", "example-app": "^0.2.32", "express": "^4.17.1", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ab7ab1f552..0a0ccd36b1 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -40,7 +40,7 @@ "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", "@types/git-url-parse": "^9.0.0", - "azure-devops-node-api": "^10.1.1", + "azure-devops-node-api": "^10.2.2", "command-exists": "^1.2.9", "compression": "^1.7.4", "cors": "^2.8.5", diff --git a/yarn.lock b/yarn.lock index 27a6fcc250..ae7fa6c4f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8036,14 +8036,13 @@ axobject-query@^2.2.0: resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== -azure-devops-node-api@^10.1.1: - version "10.2.1" - resolved "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-10.2.1.tgz#835080164f8c30cec6506c47198b044c053f1f36" - integrity sha512-XuSiUaYpk0tQpd9fD8qfRa5y1IdavupKNVmwxy0w/RhmxG2Wl8uAYnNJchUoWd3Rn9On0mYTCCZSn+UlYdYFSg== +azure-devops-node-api@^10.2.2: + version "10.2.2" + resolved "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-10.2.2.tgz#9f557e622dd07bbaa9bd5e7e84e17c761e2151b2" + integrity sha512-4TVv2X7oNStT0vLaEfExmy3J4/CzfuXolEcQl/BRUmvGySqKStTG2O55/hUQ0kM7UJlZBLgniM0SBq4d/WkKow== dependencies: tunnel "0.0.6" - typed-rest-client "^1.8.0" - underscore "1.8.3" + typed-rest-client "^1.8.4" babel-code-frame@^6.22.0: version "6.26.0" @@ -25697,14 +25696,14 @@ type@^2.0.0: resolved "https://registry.npmjs.org/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3" integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow== -typed-rest-client@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.0.tgz#3b6c22a7cc31b665ec1e4bedb3482ebe12e2fbe6" - integrity sha512-Nu1MrdH6ECrRW5gHoRAdubgCs4oH6q5/J76jsEC8bVDfvVoVPkigukPalhMHPwb7ZvpsZqPptd5zpt/QdtrdBw== +typed-rest-client@^1.8.4: + version "1.8.4" + resolved "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.4.tgz#ba3fb788e5b9322547406392533f12d660a5ced6" + integrity sha512-MyfKKYzk3I6/QQp6e1T50py4qg+c+9BzOEl2rBmQIpStwNUoqQ73An+Tkfy9YuV7O+o2mpVVJpe+fH//POZkbg== dependencies: qs "^6.9.1" tunnel "0.0.6" - underscore "1.8.3" + underscore "^1.12.1" typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -25803,10 +25802,10 @@ undefsafe@^2.0.3: dependencies: debug "^2.2.0" -underscore@1.8.3: - version "1.8.3" - resolved "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" - integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= +underscore@^1.12.1: + version "1.13.1" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" + integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== underscore@^1.9.1: version "1.11.0" From f7134c368419d1b43733f6aa0c4854d2fceef62f Mon Sep 17 00:00:00 2001 From: Alex Landovskis Date: Tue, 6 Jul 2021 19:48:03 -0400 Subject: [PATCH 05/47] chore: bump sqlite3 to 5.0.1 Signed-off-by: Alex Landovskis --- .changeset/healthy-colts-watch.md | 8 ++++++++ packages/backend-test-utils/package.json | 2 +- packages/backend/package.json | 2 +- .../packages/backend/package.json.hbs | 2 +- plugins/catalog-backend/package.json | 2 +- yarn.lock | 18 +++++++++--------- 6 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 .changeset/healthy-colts-watch.md diff --git a/.changeset/healthy-colts-watch.md b/.changeset/healthy-colts-watch.md new file mode 100644 index 0000000000..763f2a9828 --- /dev/null +++ b/.changeset/healthy-colts-watch.md @@ -0,0 +1,8 @@ +--- +'example-backend': patch +'@backstage/backend-test-utils': patch +'@backstage/create-app': patch +'@backstage/plugin-catalog-backend': patch +--- + +bump sqlite3 to 5.0.1 diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 3079041c7f..65852c3a4f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -36,7 +36,7 @@ "knex": "^0.95.1", "mysql2": "^2.2.5", "pg": "^8.3.0", - "sqlite3": "^5.0.0", + "sqlite3": "^5.0.1", "testcontainers": "^7.10.0", "uuid": "^8.0.0" }, diff --git a/packages/backend/package.json b/packages/backend/package.json index a2c5906696..f524636db1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -56,7 +56,7 @@ "knex": "^0.95.1", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", - "sqlite3": "^5.0.0", + "sqlite3": "^5.0.1", "winston": "^3.2.1" }, "devDependencies": { diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 8f5a116de4..44af516099 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -40,7 +40,7 @@ "pg": "^8.3.0", {{/if}} {{#if dbTypeSqlite}} - "sqlite3": "^5.0.0", + "sqlite3": "^5.0.1", {{/if}} "winston": "^3.2.1" }, diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 2a8588c964..13cd430d01 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -71,7 +71,7 @@ "@types/uuid": "^8.0.0", "@types/yup": "^0.29.8", "msw": "^0.29.0", - "sqlite3": "^5.0.0", + "sqlite3": "^5.0.1", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2" }, diff --git a/yarn.lock b/yarn.lock index 27a6fcc250..1789c11047 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19107,10 +19107,10 @@ node-abi@^2.7.0: dependencies: semver "^5.4.1" -node-addon-api@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" - integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA== +node-addon-api@^3.0.0: + version "3.2.1" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== node-cache@^5.1.2: version "5.1.2" @@ -24066,12 +24066,12 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sqlite3@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz#1bfef2151c6bc48a3ab1a6c126088bb8dd233566" - integrity sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw== +sqlite3@^5.0.1: + version "5.0.2" + resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083" + integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA== dependencies: - node-addon-api "2.0.0" + node-addon-api "^3.0.0" node-pre-gyp "^0.11.0" optionalDependencies: node-gyp "3.x" From 921e3ca569c7a81a20ab2b5e9c271ef822a1efae Mon Sep 17 00:00:00 2001 From: Chase Rutherford-Jenkins Date: Tue, 29 Jun 2021 22:10:56 -0700 Subject: [PATCH 06/47] use configured git author in scaffolder actions Co-authored-by: Himanshu Mishra Signed-off-by: Chase Rutherford-Jenkins --- app-config.yaml | 5 +++ plugins/scaffolder-backend/api-report.md | 5 +++ plugins/scaffolder-backend/config.d.ts | 9 ++++ .../actions/builtin/createBuiltinActions.ts | 8 +++- .../actions/builtin/publish/azure.test.ts | 23 +++++----- .../actions/builtin/publish/azure.ts | 10 ++++- .../actions/builtin/publish/bitbucket.test.ts | 43 ++++++++++--------- .../actions/builtin/publish/bitbucket.ts | 10 ++++- .../actions/builtin/publish/github.test.ts | 24 ++++++----- .../actions/builtin/publish/github.ts | 10 ++++- .../actions/builtin/publish/gitlab.test.ts | 37 ++++++++-------- .../actions/builtin/publish/gitlab.ts | 10 ++++- .../src/scaffolder/stages/publish/helpers.ts | 12 +++++- .../scaffolder-backend/src/service/router.ts | 1 + 14 files changed, 139 insertions(+), 68 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index e3ab12587e..c0bea41b34 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -250,6 +250,11 @@ catalog: target: ../catalog-model/examples/acme-corp.yaml scaffolder: + # Use to customize commit author info used when new components are created + # git: + # author: + # name: Scaffolder + # email: scaffolder@backstage.io github: token: ${GITHUB_TOKEN} visibility: public # or 'internal' or 'private' diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 7528388a47..21c6e03b0f 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -116,6 +116,7 @@ export const createBuiltinActions: (options: { integrations: ScmIntegrations; catalogClient: CatalogApi; templaters: TemplaterBuilder; + config: Config; }) => TemplateAction[]; // @public (undocumented) @@ -155,11 +156,13 @@ export function createLegacyActions(options: Options): TemplateAction[]; // @public (undocumented) export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }): TemplateAction; // @public (undocumented) export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }): TemplateAction; // @public @@ -168,6 +171,7 @@ export function createPublishFileAction(): TemplateAction; // @public (undocumented) export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }): TemplateAction; // @public (undocumented) @@ -176,6 +180,7 @@ export const createPublishGithubPullRequestAction: ({ integrations, clientFactor // @public (undocumented) export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }): TemplateAction; // @public (undocumented) diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 971c3036d6..07602b649a 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -17,6 +17,15 @@ export interface Config { /** Configuration options for the scaffolder plugin */ scaffolder?: { + git?: { + /** + * The commit author info used when new components are created. + */ + author?: { + name?: string; + email?: string; + }; + }; github?: { [key: string]: string; /** diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 46b8db72b4..b3757571ff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -17,6 +17,7 @@ import { UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { ScmIntegrations } from '@backstage/integration'; +import { Config } from '@backstage/config'; import { TemplaterBuilder } from '../../stages'; import { createCatalogRegisterAction, @@ -41,8 +42,9 @@ export const createBuiltinActions = (options: { integrations: ScmIntegrations; catalogClient: CatalogApi; templaters: TemplaterBuilder; + config: Config; }) => { - const { reader, integrations, templaters, catalogClient } = options; + const { reader, integrations, templaters, catalogClient, config } = options; return [ createFetchPlainAction({ @@ -56,18 +58,22 @@ export const createBuiltinActions = (options: { }), createPublishGithubAction({ integrations, + config, }), createPublishGithubPullRequestAction({ integrations, }), createPublishGitlabAction({ integrations, + config, }), createPublishBitbucketAction({ integrations, + config, }), createPublishAzureAction({ integrations, + config, }), createDebugLogAction(), createCatalogRegisterAction({ catalogClient, integrations }), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index d3cb4ccafc..6a1c8fdc55 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -28,17 +28,17 @@ import { PassThrough } from 'stream'; import { initRepoAndPush } from '../../../stages/publish/helpers'; describe('publish:azure', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); - const action = createPublishAzureAction({ integrations }); + const config = new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishAzureAction({ integrations, config }); const mockContext = { input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org', @@ -187,6 +187,7 @@ describe('publish:azure', () => { defaultBranch: 'master', auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index eb1b0522c6..677abc3a08 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -21,11 +21,13 @@ import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/Git import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; +import { Config } from '@backstage/config'; export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }) { - const { integrations } = options; + const { integrations, config } = options; return createTemplateAction<{ repoUrl: string; @@ -123,6 +125,11 @@ export function createPublishAzureAction(options: { // so it's just the base path I think const repoContentsUrl = remoteUrl; + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.git.author.name'), + email: config.getOptionalString('scaffolder.git.author.email'), + }; + await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, @@ -132,6 +139,7 @@ export function createPublishAzureAction(options: { password: integrationConfig.config.token, }, logger: ctx.logger, + gitAuthorInfo, }); ctx.output('remoteUrl', remoteUrl); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 16b84737a7..f173959aa0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -26,27 +26,27 @@ import { PassThrough } from 'stream'; import { initRepoAndPush } from '../../../stages/publish/helpers'; describe('publish:bitbucket', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - token: 'tokenlols', - }, - { - host: 'hosted.bitbucket.com', - token: 'thing', - apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', - }, - { - host: 'notoken.bitbucket.com', - }, - ], - }, - }), - ); - const action = createPublishBitbucketAction({ integrations }); + const config = new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + token: 'tokenlols', + }, + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'notoken.bitbucket.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketAction({ integrations, config }); const mockContext = { input: { repoUrl: 'bitbucket.org?repo=repo&owner=owner', @@ -331,6 +331,7 @@ describe('publish:bitbucket', () => { defaultBranch: 'main', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 295f4d1574..b05b51a7eb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -23,6 +23,7 @@ import fetch from 'cross-fetch'; import { initRepoAndPush } from '../../../stages/publish/helpers'; import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; +import { Config } from '@backstage/config'; const createBitbucketCloudRepository = async (opts: { owner: string; @@ -184,8 +185,9 @@ const performEnableLFS = async (opts: { export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }) { - const { integrations } = options; + const { integrations, config } = options; return createTemplateAction<{ repoUrl: string; @@ -284,6 +286,11 @@ export function createPublishBitbucketAction(options: { apiBaseUrl, }); + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.git.author.name'), + email: config.getOptionalString('scaffolder.git.author.email'), + }; + await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, @@ -297,6 +304,7 @@ export function createPublishBitbucketAction(options: { }, defaultBranch, logger: ctx.logger, + gitAuthorInfo, }); if (enableLFS && host !== 'bitbucket.org') { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 910fa27b46..058168d666 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -25,17 +25,17 @@ import { initRepoAndPush } from '../../../stages/publish/helpers'; import { when } from 'jest-when'; describe('publish:github', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { host: 'github.com', token: 'tokenlols' }, - { host: 'ghe.github.com' }, - ], - }, - }), - ); - const action = createPublishGithubAction({ integrations }); + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGithubAction({ integrations, config }); const mockContext = { input: { repoUrl: 'github.com?repo=repo&owner=owner', @@ -178,6 +178,7 @@ describe('publish:github', () => { defaultBranch: 'master', auth: { username: 'x-access-token', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); @@ -207,6 +208,7 @@ describe('publish:github', () => { defaultBranch: 'main', auth: { username: 'x-access-token', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 7dd790cb62..f14dd817b0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -25,14 +25,16 @@ import { } from '../../../stages/publish/helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; +import { Config } from '@backstage/config'; type Permission = 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; type Collaborator = { access: Permission; username: string }; export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }) { - const { integrations } = options; + const { integrations, config } = options; const credentialsProviders = new Map( integrations.github.list().map(integration => { @@ -248,6 +250,11 @@ export function createPublishGithubAction(options: { const remoteUrl = newRepo.clone_url; const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.git.author.name'), + email: config.getOptionalString('scaffolder.git.author.email'), + }; + await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, @@ -257,6 +264,7 @@ export function createPublishGithubAction(options: { password: token, }, logger: ctx.logger, + gitAuthorInfo, }); try { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 67064efde7..427056454c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -24,24 +24,24 @@ import { PassThrough } from 'stream'; import { initRepoAndPush } from '../../../stages/publish/helpers'; describe('publish:gitlab', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'tokenlols', - apiBaseUrl: 'https://api.gitlab.com', - }, - { - host: 'hosted.gitlab.com', - apiBaseUrl: 'https://api.hosted.gitlab.com', - }, - ], - }, - }), - ); - const action = createPublishGitlabAction({ integrations }); + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGitlabAction({ integrations, config }); const mockContext = { input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', @@ -166,6 +166,7 @@ describe('publish:gitlab', () => { remoteUrl: 'http://mockurl.git', auth: { username: 'oauth2', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 57c7fdf752..6d7384294d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -20,11 +20,13 @@ import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from '../../../stages/publish/helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; +import { Config } from '@backstage/config'; export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; + config: Config; }) { - const { integrations } = options; + const { integrations, config } = options; return createTemplateAction<{ repoUrl: string; @@ -121,6 +123,11 @@ export function createPublishGitlabAction(options: { const remoteUrl = (http_url_to_repo as string).replace(/\.git$/, ''); const repoContentsUrl = `${remoteUrl}/-/blob/master`; + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.git.author.name'), + email: config.getOptionalString('scaffolder.git.author.email'), + }; + await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl: http_url_to_repo as string, @@ -130,6 +137,7 @@ export function createPublishGitlabAction(options: { password: integrationConfig.config.token, }, logger: ctx.logger, + gitAuthorInfo, }); ctx.output('remoteUrl', remoteUrl); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts index dff0feb5f2..c974c466d4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts @@ -25,12 +25,14 @@ export async function initRepoAndPush({ auth, logger, defaultBranch = 'master', + gitAuthorInfo, }: { dir: string; remoteUrl: string; auth: { username: string; password: string }; logger: Logger; defaultBranch?: string; + gitAuthorInfo?: { name?: string; email?: string }; }): Promise { const git = Git.fromAuth({ username: auth.username, @@ -53,11 +55,17 @@ export async function initRepoAndPush({ await git.add({ dir, filepath }); } + // use provided info if possible, otherwise use fallbacks + const authorInfo = { + name: gitAuthorInfo?.name ?? 'Scaffolder', + email: gitAuthorInfo?.email ?? 'scaffolder@backstage.io', + }; + await git.commit({ dir, message: 'Initial commit', - author: { name: 'Scaffolder', email: 'scaffolder@backstage.io' }, - committer: { name: 'Scaffolder', email: 'scaffolder@backstage.io' }, + author: authorInfo, + committer: authorInfo, }); await git.addRemote({ diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index b0142d4b02..17304aefb1 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -129,6 +129,7 @@ export async function createRouter( catalogClient, templaters, reader, + config, }), ]; From b1aca39abe11bc22f0908fecf2d53dc95a1b58c5 Mon Sep 17 00:00:00 2001 From: Chase Rutherford-Jenkins Date: Fri, 2 Jul 2021 17:11:32 -0500 Subject: [PATCH 07/47] incorporate naming feedback; add tests Signed-off-by: Chase Rutherford-Jenkins --- app-config.yaml | 9 ++- plugins/scaffolder-backend/config.d.ts | 14 ++-- .../actions/builtin/publish/azure.test.ts | 39 +++++++++++ .../actions/builtin/publish/azure.ts | 4 +- .../actions/builtin/publish/bitbucket.test.ts | 69 +++++++++++++++++++ .../actions/builtin/publish/bitbucket.ts | 4 +- .../actions/builtin/publish/github.test.ts | 47 +++++++++++++ .../actions/builtin/publish/github.ts | 4 +- .../actions/builtin/publish/gitlab.test.ts | 47 +++++++++++++ .../actions/builtin/publish/gitlab.ts | 4 +- 10 files changed, 220 insertions(+), 21 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index c0bea41b34..b50de963ed 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -250,11 +250,10 @@ catalog: target: ../catalog-model/examples/acme-corp.yaml scaffolder: - # Use to customize commit author info used when new components are created - # git: - # author: - # name: Scaffolder - # email: scaffolder@backstage.io + # Use to customize default commit author info used when new components are created + # defaultAuthor: + # name: Scaffolder + # email: scaffolder@backstage.io github: token: ${GITHUB_TOKEN} visibility: public # or 'internal' or 'private' diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 07602b649a..795fe5b112 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -17,14 +17,12 @@ export interface Config { /** Configuration options for the scaffolder plugin */ scaffolder?: { - git?: { - /** - * The commit author info used when new components are created. - */ - author?: { - name?: string; - email?: string; - }; + /** + * The commit author info used when new components are created. + */ + defaultAuthor?: { + name?: string; + email?: string; }; github?: { [key: string]: string; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 6a1c8fdc55..b5c9bdcd89 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -191,6 +191,45 @@ describe('publish:azure', () => { }); }); + it('should call initRepoAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = ScmIntegrations.fromConfig( + customAuthorConfig, + ); + const customAuthorAction = createPublishAzureAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + mockGitClient.createRepository.mockImplementation(() => ({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + })); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + it('should call output with the remoteUrl and the repoContentsUrl', async () => { mockGitClient.createRepository.mockImplementation(() => ({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 677abc3a08..8b8f6ce3e5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -126,8 +126,8 @@ export function createPublishAzureAction(options: { const repoContentsUrl = remoteUrl; const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.git.author.name'), - email: config.getOptionalString('scaffolder.git.author.email'), + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; await initRepoAndPush({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index f173959aa0..6a72cf1bc3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -335,6 +335,75 @@ describe('publish:bitbucket', () => { }); }); + it('should call initAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + token: 'tokenlols', + }, + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'notoken.bitbucket.com', + }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = ScmIntegrations.fromConfig( + customAuthorConfig, + ); + const customAuthorAction = createPublishBitbucketAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/owner/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/owner/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/owner/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/owner/cloneurl', + auth: { username: 'x-token-auth', password: 'tokenlols' }, + logger: mockContext.logger, + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + it('should call outputs with the correct urls', async () => { server.use( rest.post( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index b05b51a7eb..1dc80f370c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -287,8 +287,8 @@ export function createPublishBitbucketAction(options: { }); const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.git.author.name'), - email: config.getOptionalString('scaffolder.git.author.email'), + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; await initRepoAndPush({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 058168d666..0b277c4739 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -212,6 +212,53 @@ describe('publish:github', () => { }); }); + it('should call initRepoAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = ScmIntegrations.fromConfig( + customAuthorConfig, + ); + const customAuthorAction = createPublishGithubAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + it('should add access for the team when it starts with the owner', async () => { mockGithubClient.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index f14dd817b0..9f693a96d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -251,8 +251,8 @@ export function createPublishGithubAction(options: { const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.git.author.name'), - email: config.getOptionalString('scaffolder.git.author.email'), + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; await initRepoAndPush({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 427056454c..08ab87c271 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -170,6 +170,53 @@ describe('publish:gitlab', () => { }); }); + it('should call initRepoAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = ScmIntegrations.fromConfig( + customAuthorConfig, + ); + const customAuthorAction = createPublishGitlabAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Projects.create.mockResolvedValue({ + http_url_to_repo: 'http://mockurl.git', + }); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'http://mockurl.git', + auth: { username: 'oauth2', password: 'tokenlols' }, + logger: mockContext.logger, + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + it('should call output with the remoteUrl and repoContentsUrl', async () => { mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); mockGitlabClient.Projects.create.mockResolvedValue({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 6d7384294d..1cd77ebe2a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -124,8 +124,8 @@ export function createPublishGitlabAction(options: { const repoContentsUrl = `${remoteUrl}/-/blob/master`; const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.git.author.name'), - email: config.getOptionalString('scaffolder.git.author.email'), + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; await initRepoAndPush({ From 7cad18e2f039e71161af7aa582f1fd758b7d2546 Mon Sep 17 00:00:00 2001 From: Chase Rutherford-Jenkins Date: Tue, 6 Jul 2021 20:45:10 -0500 Subject: [PATCH 08/47] add changeset Signed-off-by: Chase Rutherford-Jenkins --- .changeset/moody-garlics-whisper.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .changeset/moody-garlics-whisper.md diff --git a/.changeset/moody-garlics-whisper.md b/.changeset/moody-garlics-whisper.md new file mode 100644 index 0000000000..b2867bc016 --- /dev/null +++ b/.changeset/moody-garlics-whisper.md @@ -0,0 +1,24 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Adding `config: Config` as a required argument to `createBuiltinActions` and downstream methods in order to support configuration of the default git author used for Scaffolder commits. + +The affected methods are: + +- `createBuiltinActions` +- `createPublishGithubAction` +- `createPublishGitlabAction` +- `createPublishBitbucketAction` +- `createPublishAzureAction` + +Call sites to these methods will need to be migrated to include the new `config` argument. See `createRouter` in `plugins/scaffolder-backend/src/service/router.ts` for an example of adding this new argument. + +To configure the default git author, use the `defaultAuthor` key under `scaffolder` in `app-config.yaml`: + +```yaml +scaffolder: + defaultAuthor: + name: Example + email: example@example.com +``` From 3c19f1230f28d2465a83410fca6ed9dadbb9e9c0 Mon Sep 17 00:00:00 2001 From: Chase Rutherford-Jenkins Date: Tue, 6 Jul 2021 21:15:14 -0500 Subject: [PATCH 09/47] update tests Signed-off-by: Chase Rutherford-Jenkins --- .../src/scaffolder/actions/builtin/publish/azure.test.ts | 2 ++ .../src/scaffolder/actions/builtin/publish/bitbucket.test.ts | 2 ++ .../src/scaffolder/actions/builtin/publish/gitlab.test.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index b5c9bdcd89..1dc29ea763 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -165,6 +165,7 @@ describe('publish:azure', () => { defaultBranch: 'master', auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); @@ -226,6 +227,7 @@ describe('publish:azure', () => { remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, + defaultBranch: 'master', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 6a72cf1bc3..0e5f8df72b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -289,6 +289,7 @@ describe('publish:bitbucket', () => { defaultBranch: 'master', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); @@ -400,6 +401,7 @@ describe('publish:bitbucket', () => { remoteUrl: 'https://bitbucket.org/owner/cloneurl', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, + defaultBranch: 'master', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 08ab87c271..25a129d905 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -143,6 +143,7 @@ describe('publish:gitlab', () => { remoteUrl: 'http://mockurl.git', auth: { username: 'oauth2', password: 'tokenlols' }, logger: mockContext.logger, + gitAuthorInfo: {}, }); }); @@ -213,6 +214,7 @@ describe('publish:gitlab', () => { remoteUrl: 'http://mockurl.git', auth: { username: 'oauth2', password: 'tokenlols' }, logger: mockContext.logger, + defaultBranch: 'master', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, }); }); From ca46883c3a4a6a702ce8aa738575b524a6d4c8ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jul 2021 04:10:11 +0000 Subject: [PATCH 10/47] chore(deps): bump passport-oauth2 from 1.5.0 to 1.6.0 Bumps [passport-oauth2](https://github.com/jaredhanson/passport-oauth2) from 1.5.0 to 1.6.0. - [Release notes](https://github.com/jaredhanson/passport-oauth2/releases) - [Changelog](https://github.com/jaredhanson/passport-oauth2/blob/master/CHANGELOG.md) - [Commits](https://github.com/jaredhanson/passport-oauth2/compare/v1.5.0...v1.6.0) --- updated-dependencies: - dependency-name: passport-oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a6fcc250..3eb7874b4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20313,9 +20313,9 @@ passport-oauth2@1.2.0: uid2 "0.0.x" passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" - integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== + version "1.6.0" + resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.0.tgz#5f599735e0ea40ea3027643785f81a3a9b4feb50" + integrity sha512-emXPLqLcVEcLFR/QvQXZcwLmfK8e9CqvMgmOFJxcNT3okSFMtUbRRKpY20x5euD+01uHsjjCa07DYboEeLXYiw== dependencies: base64url "3.x.x" oauth "0.9.x" From e056e9d88b8e93968ada74fd137a96cff70e1940 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jul 2021 04:13:40 +0000 Subject: [PATCH 11/47] chore(deps-dev): bump @graphql-codegen/cli from 1.21.5 to 1.21.6 Bumps [@graphql-codegen/cli](https://github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/graphql-codegen-cli) from 1.21.5 to 1.21.6. - [Release notes](https://github.com/dotansimha/graphql-code-generator/releases) - [Changelog](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/graphql-codegen-cli/CHANGELOG.md) - [Commits](https://github.com/dotansimha/graphql-code-generator/commits/@graphql-codegen/cli@1.21.6/packages/graphql-codegen-cli) --- updated-dependencies: - dependency-name: "@graphql-codegen/cli" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a6fcc250..a46e778db1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2015,9 +2015,9 @@ subscriptions-transport-ws "^0.9.18" "@graphql-codegen/cli@^1.21.3": - version "1.21.5" - resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.21.5.tgz#b9041553747cfb2dee7c3473a2e2461ec3e7ada5" - integrity sha512-w3SovNJ9qtMhFLAdPZeCdGvHXDgfdb53mueWDTyncOt04m+tohVnY4qExvyKLTN5zlGxrA/5ubp2x8Az0xQarA== + version "1.21.6" + resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.21.6.tgz#d06b5f6cb625541f3981d69f99966e520b958072" + integrity sha512-wtBk4lk/YxG6MrxnBOxE9nCfR9PNDjaqA8CF9hi6Q8jiSl4sV03tC2R+gE7+2EI8J6Xa1bxZe15LnBhVwb/mUA== dependencies: "@graphql-codegen/core" "1.17.10" "@graphql-codegen/plugin-helpers" "^1.18.7" @@ -2034,7 +2034,7 @@ ansi-escapes "^4.3.1" chalk "^4.1.0" change-case-all "1.0.14" - chokidar "^3.5.1" + chokidar "^3.5.2" common-tags "^1.8.0" cosmiconfig "^7.0.0" debounce "^1.2.0" @@ -2053,7 +2053,7 @@ mkdirp "^1.0.4" string-env-interpolation "^1.0.1" ts-log "^2.2.3" - tslib "~2.2.0" + tslib "~2.3.0" valid-url "^1.0.9" wrap-ansi "^7.0.0" yaml "^1.10.0" @@ -7448,7 +7448,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.3, anymatch@~3.1.1: +anymatch@^3.0.3: version "3.1.1" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== @@ -7456,6 +7456,14 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + apollo-cache-control@^0.14.0: version "0.14.0" resolved "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.14.0.tgz#95f20c3e03e7994e0d1bd48c59aeaeb575ed0ce7" @@ -9388,20 +9396,20 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.1: - version "3.5.1" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" + fsevents "~2.3.2" chownr@^1.1.1: version "1.1.4" @@ -13552,7 +13560,7 @@ fsevents@^2.1.2: resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== -fsevents@~2.3.1: +fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -13895,7 +13903,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0: +glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -22515,10 +22523,10 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" From d9c6637b385ed5dd8d65a1c5cef6eca23765142d Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 7 Jul 2021 10:28:47 +0200 Subject: [PATCH 12/47] Update sweet-terms-approve.md Signed-off-by: Ben Lambert --- .changeset/sweet-terms-approve.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/sweet-terms-approve.md b/.changeset/sweet-terms-approve.md index 56297f694d..e8daa06673 100644 --- a/.changeset/sweet-terms-approve.md +++ b/.changeset/sweet-terms-approve.md @@ -1,5 +1,4 @@ --- -'example-backend': patch '@backstage/plugin-scaffolder-backend': patch --- From 313aa8b926c718dfc0be667075588085878a654b Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 7 Jul 2021 10:30:04 +0200 Subject: [PATCH 13/47] Update healthy-colts-watch.md Signed-off-by: Ben Lambert --- .changeset/healthy-colts-watch.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/healthy-colts-watch.md b/.changeset/healthy-colts-watch.md index 763f2a9828..2b24621f20 100644 --- a/.changeset/healthy-colts-watch.md +++ b/.changeset/healthy-colts-watch.md @@ -1,5 +1,4 @@ --- -'example-backend': patch '@backstage/backend-test-utils': patch '@backstage/create-app': patch '@backstage/plugin-catalog-backend': patch From b254ebff8176b7464166414814016f4c4e61eefb Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 7 Jul 2021 10:43:11 +0200 Subject: [PATCH 14/47] Update moody-garlics-whisper.md Signed-off-by: Ben Lambert --- .changeset/moody-garlics-whisper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/moody-garlics-whisper.md b/.changeset/moody-garlics-whisper.md index b2867bc016..b9683e4637 100644 --- a/.changeset/moody-garlics-whisper.md +++ b/.changeset/moody-garlics-whisper.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend': minor --- Adding `config: Config` as a required argument to `createBuiltinActions` and downstream methods in order to support configuration of the default git author used for Scaffolder commits. From 538ada54efa0e38e9af333348da73715c002ea8c Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Wed, 7 Jul 2021 21:21:01 +1200 Subject: [PATCH 15/47] fix: add uiSchema support for dependent forms Signed-off-by: Jos Craw --- .../MultistepJsonForm/schema.test.ts | 60 +++++++++++++++++++ .../components/MultistepJsonForm/schema.ts | 12 +++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts index 1031dfffe3..b51d01f719 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts @@ -286,4 +286,64 @@ describe('transformSchemaToProps', () => { uiSchema: expectedUiSchema, }); }); + + it('transforms schema with dependencies', () => { + const inputSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + credit_card: { + type: 'number', + }, + }, + required: ['name'], + dependencies: { + credit_card: { + properties: { + billing_address: { + type: 'string', + 'ui:widget': 'textarea', + }, + }, + required: ['billing_address'], + }, + }, + }; + const expectedSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + credit_card: { + type: 'number', + }, + }, + required: ['name'], + dependencies: { + credit_card: { + properties: { + billing_address: { + type: 'string', + }, + }, + required: ['billing_address'], + }, + }, + }; + const expectedUiSchema = { + billing_address: { + 'ui:widget': 'textarea', + }, + credit_card: {}, + name: {}, + }; + + expect(transformSchemaToProps(inputSchema)).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); }); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts index 3342e3a046..ee2dbce406 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts @@ -26,7 +26,7 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { return; } - const { properties, anyOf, oneOf, allOf } = schema; + const { properties, anyOf, oneOf, allOf, dependencies } = schema; for (const propName in schema) { if (!schema.hasOwnProperty(propName)) { @@ -81,6 +81,16 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { extractUiSchema(schemaNode, uiSchema); } } + + if (isObject(dependencies)) { + for (const depName of Object.keys(dependencies)) { + const schemaNode = dependencies[depName]; + if (!isObject(schemaNode)) { + continue; + } + extractUiSchema(schemaNode, uiSchema); + } + } } export function transformSchemaToProps( From 6a09ed555d092aec01700375bc18b89d35f08b51 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 19 Jun 2021 02:17:01 +0200 Subject: [PATCH 16/47] breaking: removing alphav1 support for templates Signed-off-by: blam --- packages/backend/src/plugins/scaffolder.ts | 30 +- .../src/kinds/TemplateEntityV1alpha1.test.ts | 106 ------ .../src/kinds/TemplateEntityV1alpha1.ts | 36 -- packages/catalog-model/src/kinds/index.ts | 5 - .../kinds/Template.v1alpha1.schema.json | 99 ------ .../src/ingestion/LocationReaders.ts | 1 + .../BuiltinKindsEntityProcessor.test.ts | 19 +- .../processors/BuiltinKindsEntityProcessor.ts | 6 +- .../DefaultCatalogProcessingOrchestrator.ts | 1 + .../sample-templates/remote-templates.yaml | 2 +- .../src/lib/catalog/CatalogEntityClient.ts | 9 +- .../actions/builtin/createBuiltinActions.ts | 18 +- .../actions/builtin/fetch/cookiecutter.ts | 110 +++++- .../publish => actions/builtin}/helpers.ts | 41 +++ .../actions/builtin/publish/azure.ts | 2 +- .../actions/builtin/publish/bitbucket.ts | 2 +- .../actions/builtin/publish/github.ts | 2 +- .../actions/builtin/publish/gitlab.ts | 2 +- .../src/scaffolder/index.ts | 3 - .../src/scaffolder/jobs/index.ts | 17 - .../src/scaffolder/jobs/logger.test.ts | 49 --- .../src/scaffolder/jobs/logger.ts | 48 --- .../src/scaffolder/jobs/processor.test.ts | 329 ----------------- .../src/scaffolder/jobs/processor.ts | 180 ---------- .../src/scaffolder/jobs/types.ts | 68 ---- .../src/scaffolder/stages/helpers.test.ts | 332 ------------------ .../src/scaffolder/stages/helpers.ts | 62 ---- .../src/scaffolder/stages/index.ts | 19 - .../src/scaffolder/stages/legacy.ts | 105 ------ .../scaffolder/stages/prepare/azure.test.ts | 116 ------ .../src/scaffolder/stages/prepare/azure.ts | 63 ---- .../stages/prepare/bitbucket.test.ts | 113 ------ .../scaffolder/stages/prepare/bitbucket.ts | 82 ----- .../scaffolder/stages/prepare/file.test.ts | 79 ----- .../src/scaffolder/stages/prepare/file.ts | 37 -- .../scaffolder/stages/prepare/github.test.ts | 97 ----- .../src/scaffolder/stages/prepare/github.ts | 71 ---- .../scaffolder/stages/prepare/gitlab.test.ts | 82 ----- .../src/scaffolder/stages/prepare/gitlab.ts | 62 ---- .../src/scaffolder/stages/prepare/index.ts | 23 -- .../stages/prepare/preparers.test.ts | 50 --- .../scaffolder/stages/prepare/preparers.ts | 81 ----- .../src/scaffolder/stages/prepare/types.ts | 40 --- .../scaffolder/stages/publish/azure.test.ts | 89 ----- .../src/scaffolder/stages/publish/azure.ts | 83 ----- .../stages/publish/bitbucket.test.ts | 228 ------------ .../scaffolder/stages/publish/bitbucket.ts | 207 ----------- .../scaffolder/stages/publish/github.test.ts | 315 ----------------- .../src/scaffolder/stages/publish/github.ts | 178 ---------- .../scaffolder/stages/publish/gitlab.test.ts | 151 -------- .../src/scaffolder/stages/publish/gitlab.ts | 106 ------ .../src/scaffolder/stages/publish/index.ts | 27 -- .../stages/publish/publishers.test.ts | 127 ------- .../scaffolder/stages/publish/publishers.ts | 113 ------ .../src/scaffolder/stages/publish/types.ts | 47 --- .../stages/templater/cookiecutter.test.ts | 279 --------------- .../stages/templater/cookiecutter.ts | 107 ------ .../scaffolder/stages/templater/cra/index.ts | 159 --------- .../scaffolder/stages/templater/helpers.ts | 75 ---- .../src/scaffolder/stages/templater/index.ts | 20 -- .../stages/templater/templaters.test.ts | 43 --- .../scaffolder/stages/templater/templaters.ts | 39 -- .../src/scaffolder/stages/templater/types.ts | 73 ---- .../src/scaffolder/tasks/TemplateConverter.ts | 101 ------ .../scaffolder-backend/src/service/router.ts | 113 +----- 65 files changed, 194 insertions(+), 5185 deletions(-) delete mode 100644 packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts delete mode 100644 packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts delete mode 100644 packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json rename plugins/scaffolder-backend/src/scaffolder/{stages/publish => actions/builtin}/helpers.ts (77%) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/types.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/cra/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 09b3732301..619134a990 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -14,19 +14,9 @@ * limitations under the License. */ -import { - DockerContainerRunner, - SingleHostDiscovery, -} from '@backstage/backend-common'; +import { DockerContainerRunner } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; -import { - CookieCutter, - CreateReactAppTemplater, - createRouter, - Preparers, - Publishers, - Templaters, -} from '@backstage/plugin-scaffolder-backend'; +import { createRouter } from '@backstage/plugin-scaffolder-backend'; import Docker from 'dockerode'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -36,27 +26,15 @@ export default async function createPlugin({ config, database, reader, + discovery, }: PluginEnvironment): Promise { const dockerClient = new Docker(); const containerRunner = new DockerContainerRunner({ dockerClient }); - const cookiecutterTemplater = new CookieCutter({ containerRunner }); - const craTemplater = new CreateReactAppTemplater({ containerRunner }); - const templaters = new Templaters(); - - templaters.register('cookiecutter', cookiecutterTemplater); - templaters.register('cra', craTemplater); - - const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); - - const discovery = SingleHostDiscovery.fromConfig(config); const catalogClient = new CatalogClient({ discoveryApi: discovery }); return await createRouter({ - preparers, - templaters, - publishers, + containerRunner, logger, config, database, diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts deleted file mode 100644 index f03519a3c6..0000000000 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - TemplateEntityV1alpha1, - templateEntityV1alpha1Validator as validator, -} from './TemplateEntityV1alpha1'; - -describe('templateEntityV1alpha1Validator', () => { - let entity: TemplateEntityV1alpha1; - - beforeEach(() => { - entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - name: 'test', - }, - spec: { - type: 'website', - templater: 'cookiecutter', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'team-a@example.com', - }, - }; - }); - - it('happy path: accepts valid data', async () => { - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('silently accepts v1beta1 as well', async () => { - (entity as any).apiVersion = 'backstage.io/v1beta1'; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('ignores unknown apiVersion', async () => { - (entity as any).apiVersion = 'backstage.io/v1beta0'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('ignores unknown kind', async () => { - (entity as any).kind = 'Wizard'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('rejects missing type', async () => { - delete (entity as any).spec.type; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('accepts any other type', async () => { - (entity as any).spec.type = 'hallo'; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects empty type', async () => { - (entity as any).spec.type = ''; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('rejects missing templater', async () => { - (entity as any).spec.templater = ''; - await expect(validator.check(entity)).rejects.toThrow(/templater/); - }); - it('accepts missing owner', async () => { - delete (entity as any).spec.owner; - await expect(validator.check(entity)).resolves.toBe(true); - }); - it('rejects empty owner', async () => { - (entity as any).spec.owner = ''; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - it('rejects wrong type owner', async () => { - (entity as any).spec.owner = 5; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); -}); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts deleted file mode 100644 index bdc6f35df2..0000000000 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Entity } from '../entity/Entity'; -import schema from '../schema/kinds/Template.v1alpha1.schema.json'; -import type { JSONSchema } from '../types'; -import { ajvCompiledJsonSchemaValidator } from './util'; - -export interface TemplateEntityV1alpha1 extends Entity { - apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; - kind: 'Template'; - spec: { - type: string; - templater: string; - path?: string; - schema: JSONSchema; - owner?: string; - }; -} - -export const templateEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index ccdf0051db..be9f7a0d6a 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -50,11 +50,6 @@ export type { SystemEntityV1alpha1 as SystemEntity, SystemEntityV1alpha1, } from './SystemEntityV1alpha1'; -export { templateEntityV1alpha1Validator } from './TemplateEntityV1alpha1'; -export type { - TemplateEntityV1alpha1 as TemplateEntity, - TemplateEntityV1alpha1, -} from './TemplateEntityV1alpha1'; export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; export type { KindValidator } from './types'; diff --git a/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json deleted file mode 100644 index 5a0c0efa73..0000000000 --- a/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "TemplateV1alpha1", - "description": "A Template describes a skeleton for use with the Scaffolder. It is used for describing what templating library is supported, and also for documenting the variables that the template requires using JSON Forms Schema.", - "examples": [ - { - "apiVersion": "backstage.io/v1alpha1", - "kind": "Template", - "metadata": { - "name": "react-ssr-template", - "title": "React SSR Template", - "description": "Next.js application skeleton for creating isomorphic web applications.", - "tags": ["recommended", "react"] - }, - "spec": { - "owner": "artist-relations-team", - "templater": "cookiecutter", - "type": "website", - "path": ".", - "schema": { - "required": ["component-id", "description"], - "properties": { - "component_id": { - "title": "Name", - "type": "string", - "description": "Unique name of the component" - }, - "description": { - "title": "Description", - "type": "string", - "description": "Description of the component" - } - } - } - } - } - ], - "allOf": [ - { - "$ref": "Entity" - }, - { - "type": "object", - "required": ["spec"], - "properties": { - "apiVersion": { - "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] - }, - "kind": { - "enum": ["Template"] - }, - "metadata": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The nice display name for the template. This field is required as is used to reference the template to the user instead of the metadata.name field.", - "examples": ["React SSR Template"], - "minLength": 1 - } - } - }, - "spec": { - "type": "object", - "required": ["type", "templater", "schema"], - "properties": { - "type": { - "type": "string", - "description": "The type of component created by the template. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.", - "examples": ["service", "website", "library"], - "minLength": 1 - }, - "templater": { - "type": "string", - "description": "The templating library that is supported by the template skeleton.", - "examples": ["cookiecutter"], - "minLength": 1 - }, - "path": { - "type": "string", - "description": "The string location where the templater should be run if it is not on the same level as the template.yaml definition.", - "examples": ["./cookiecutter/skeleton"], - "minLength": 1 - }, - "schema": { - "type": "object", - "description": "The JSONSchema describing the inputs for the template." - }, - "owner": { - "type": "string", - "description": "The user (or group) owner of the template", - "minLength": 1 - } - } - } - } - } - ] -} diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 624c7aaa61..86a8ebb9f2 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -265,6 +265,7 @@ export class LocationReaders implements LocationReader { )}, ${e}`; emit(result.inputError(item.location, message)); logger.warn(message); + logger.debug(e.stack); return undefined; } } diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts index bdc6d463c2..33beffed0d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts @@ -21,7 +21,7 @@ import { GroupEntity, ResourceEntity, SystemEntity, - TemplateEntity, + TemplateEntityV1beta2, UserEntity, } from '@backstage/catalog-model'; import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; @@ -522,22 +522,13 @@ describe('BuiltinKindsEntityProcessor', () => { }); }); it('generates relations for template entities', async () => { - const entity: TemplateEntity = { - apiVersion: 'backstage.io/v1alpha1', + const entity: TemplateEntityV1beta2 = { + apiVersion: 'backstage.io/v1beta2', kind: 'Template', metadata: { name: 'n' }, spec: { - schema: { - properties: { - description: { - title: 'd', - type: 'string', - description: 'des', - }, - }, - }, - templater: 'cookiecutter', - path: '.', + parameters: {}, + steps: [], type: 'service', owner: 'o', }, diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index b8805ab9ba..bcabc00f32 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -46,8 +46,7 @@ import { resourceEntityV1alpha1Validator, SystemEntity, systemEntityV1alpha1Validator, - TemplateEntity, - templateEntityV1alpha1Validator, + TemplateEntityV1beta2, templateEntityV1beta2Validator, UserEntity, userEntityV1alpha1Validator, @@ -62,7 +61,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { resourceEntityV1alpha1Validator, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, - templateEntityV1alpha1Validator, templateEntityV1beta2Validator, userEntityV1alpha1Validator, systemEntityV1alpha1Validator, @@ -136,7 +134,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { * Emit relations for the Template kind */ if (entity.kind === 'Template') { - const template = entity as TemplateEntity; + const template = entity as TemplateEntityV1beta2; doEmit( template.spec.owner, { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, diff --git a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts index bb23b6afb3..235673db03 100644 --- a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts @@ -124,6 +124,7 @@ export class DefaultCatalogProcessingOrchestrator }; } catch (error) { this.options.logger.warn(error.message); + this.options.logger.debug(error.stack); return { ok: false, errors: collector.results().errors.concat(error), diff --git a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml index 7846a0994e..b89b21d5e3 100644 --- a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml @@ -6,4 +6,4 @@ metadata: spec: type: url targets: - - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + # - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 9f3a754485..430afa5b5d 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - TemplateEntityV1alpha1, - TemplateEntityV1beta2, -} from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; import { ConflictError, NotFoundError } from '@backstage/errors'; @@ -35,7 +32,7 @@ export class CatalogEntityClient { async findTemplate( templateName: string, options?: { token?: string }, - ): Promise { + ): Promise { const { items: templates } = (await this.catalogClient.getEntities( { filter: { @@ -44,7 +41,7 @@ export class CatalogEntityClient { }, }, options, - )) as { items: (TemplateEntityV1alpha1 | TemplateEntityV1beta2)[] }; + )) as { items: TemplateEntityV1beta2[] }; if (templates.length !== 1) { if (templates.length > 1) { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index b3757571ff..08d7bbf824 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { ContainerRunner, UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { ScmIntegrations } from '@backstage/integration'; import { Config } from '@backstage/config'; -import { TemplaterBuilder } from '../../stages'; import { - createCatalogRegisterAction, createCatalogWriteAction, + createCatalogRegisterAction, } from './catalog'; + import { createDebugLogAction } from './debug'; import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch'; import { @@ -41,10 +41,16 @@ export const createBuiltinActions = (options: { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; - templaters: TemplaterBuilder; + containerRunner: ContainerRunner; config: Config; }) => { - const { reader, integrations, templaters, catalogClient, config } = options; + const { + reader, + integrations, + containerRunner, + catalogClient, + config, + } = options; return [ createFetchPlainAction({ @@ -54,7 +60,7 @@ export const createBuiltinActions = (options: { createFetchCookiecutterAction({ reader, integrations, - templaters, + containerRunner, }), createPublishGithubAction({ integrations, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts index 62dfc20125..e5720204c7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts @@ -14,22 +14,116 @@ * limitations under the License. */ -import { UrlReader, resolveSafeChildPath } from '@backstage/backend-common'; -import { JsonObject } from '@backstage/config'; +import { + ContainerRunner, + UrlReader, + resolveSafeChildPath, +} from '@backstage/backend-common'; +import { JsonObject, JsonValue } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; +import commandExists from 'command-exists'; import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; -import { TemplaterBuilder, TemplaterValues } from '../../../stages/templater'; +import path, { resolve as resolvePath } from 'path'; +import { Writable } from 'stream'; +import { runCommand } from '../helpers'; import { createTemplateAction } from '../../createTemplateAction'; import { fetchContents } from './helpers'; +export class CookiecutterRunner { + private readonly containerRunner: ContainerRunner; + + constructor({ containerRunner }: { containerRunner: ContainerRunner }) { + this.containerRunner = containerRunner; + } + + private async fetchTemplateCookieCutter( + directory: string, + ): Promise> { + try { + return await fs.readJSON(path.join(directory, 'cookiecutter.json')); + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + + return {}; + } + } + + public async run({ + workspacePath, + values, + logStream, + }: { + workspacePath: string; + values: JsonObject; + logStream: Writable; + }): Promise { + const templateDir = path.join(workspacePath, 'template'); + const intermediateDir = path.join(workspacePath, 'intermediate'); + await fs.ensureDir(intermediateDir); + const resultDir = path.join(workspacePath, 'result'); + + // First lets grab the default cookiecutter.json file + const cookieCutterJson = await this.fetchTemplateCookieCutter(templateDir); + + const { imageName, ...valuesForCookieCutterJson } = values; + const cookieInfo = { + ...cookieCutterJson, + ...valuesForCookieCutterJson, + }; + + await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); + + // Directories to bind on container + const mountDirs = { + [templateDir]: '/input', + [intermediateDir]: '/output', + }; + + // the command-exists package returns `true` or throws an error + const cookieCutterInstalled = await commandExists('cookiecutter').catch( + () => false, + ); + if (cookieCutterInstalled) { + await runCommand({ + command: 'cookiecutter', + args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], + logStream, + }); + } else { + await this.containerRunner.runContainer({ + imageName: (imageName as string) ?? 'spotify/backstage-cookiecutter', + command: 'cookiecutter', + args: ['--no-input', '-o', '/output', '/input', '--verbose'], + 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 cookiecutter was successful, intermediateDir will contain + // exactly one directory. + const [generated] = await fs.readdir(intermediateDir); + + if (generated === undefined) { + throw new Error('No data generated by cookiecutter'); + } + + await fs.move(path.join(intermediateDir, generated), resultDir); + } +} + export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; - templaters: TemplaterBuilder; + containerRunner: ContainerRunner; }) { - const { reader, templaters, integrations } = options; + const { reader, containerRunner, integrations } = options; return createTemplateAction<{ url: string; @@ -121,9 +215,9 @@ export function createFetchCookiecutterAction(options: { outputPath: templateContentsDir, }); - const cookiecutter = templaters.get('cookiecutter'); + const cookiecutter = new CookiecutterRunner({ containerRunner }); const values = { - ...(ctx.input.values as TemplaterValues), + ...ctx.input.values, _copy_without_render: ctx.input.copyWithoutRender, _extensions: ctx.input.extensions, imageName: ctx.input.imageName, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts similarity index 77% rename from plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts rename to plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index c974c466d4..a024be4f67 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -1,5 +1,9 @@ /* +<<<<<<< HEAD:plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts * Copyright 2020 The Backstage Authors +======= + * Copyright 2021 The Backstage Authors +>>>>>>> breaking: removing alphav1 support for templates:plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +18,48 @@ * limitations under the License. */ +import { spawn } from 'child_process'; +import { PassThrough, Writable } from 'stream'; import globby from 'globby'; import { Logger } from 'winston'; import { Git } from '@backstage/backend-common'; import { Octokit } from '@octokit/rest'; +export type RunCommandOptions = { + command: string; + args: string[]; + logStream?: Writable; +}; + +export const runCommand = async ({ + command, + args, + logStream = new PassThrough(), +}: RunCommandOptions) => { + await new Promise((resolve, reject) => { + const process = spawn(command, args); + + process.stdout.on('data', stream => { + logStream.write(stream); + }); + + process.stderr.on('data', stream => { + logStream.write(stream); + }); + + process.on('error', error => { + return reject(error); + }); + + process.on('close', code => { + if (code !== 0) { + return reject(`Command ${command} failed, exit code: ${code}`); + } + return resolve(); + }); + }); +}; + export async function initRepoAndPush({ dir, remoteUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 8b8f6ce3e5..c58dd69424 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -16,7 +16,7 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 1dc80f370c..ffd2e4d6a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -20,7 +20,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import fetch from 'cross-fetch'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 9f693a96d8..05301bda85 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -22,7 +22,7 @@ import { Octokit } from '@octokit/rest'; import { enableBranchProtectionOnDefaultRepoBranch, initRepoAndPush, -} from '../../../stages/publish/helpers'; +} from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 1cd77ebe2a..b99d40b1f7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -17,7 +17,7 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { Gitlab } from '@gitbeaker/node'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index 038873b42c..284e372b4c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -13,7 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { createLegacyActions } from './stages/legacy'; -export * from './stages'; -export * from './jobs'; export * from './actions'; diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/index.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/index.ts deleted file mode 100644 index 8ebffa2026..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from './processor'; -export * from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts deleted file mode 100644 index e5d298163b..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { makeLogStream } from './logger'; - -describe('Logger', () => { - const mockMeta = { test: 'blob' }; - - it('should return empty log lines by default', async () => { - const { log } = makeLogStream(mockMeta); - - expect(log).toEqual([]); - }); - it('should add lines to the log when using the logger that is returned', async () => { - const { logger, log } = makeLogStream(mockMeta); - - logger.info('TEST LINE'); - logger.warn('WARN LINE'); - - const [first, second] = log; - expect(log.length).toBe(2); - - expect(first).toContain('info'); - expect(first).toContain('TEST LINE'); - - expect(second).toContain('warn'); - expect(second).toContain('WARN LINE'); - }); - - it('should add lines from writing to the stream that is returned', async () => { - const { stream, log } = makeLogStream(mockMeta); - const textLine = 'SOMETHING'; - stream.write(textLine); - - expect(log).toContain(textLine); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts deleted file mode 100644 index 9ffdb33f37..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { PassThrough } from 'stream'; -import * as winston from 'winston'; -import { JsonValue } from '@backstage/config'; - -export const makeLogStream = (meta: Record) => { - const log: string[] = []; - - // Create an empty stream to collect all the log lines into - // one variable for the API. - const stream = new PassThrough(); - stream.on('data', chunk => { - const textValue = chunk.toString().trim(); - if (textValue?.length > 1) log.push(textValue); - }); - - const logger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.colorize(), - winston.format.timestamp(), - winston.format.simple(), - ), - defaultMeta: meta, - }); - - logger.add(new winston.transports.Stream({ stream })); - - return { - log, - stream, - logger, - }; -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts deleted file mode 100644 index 2bb2171899..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import parseGitUrl from 'git-url-parse'; -import mockFs from 'mock-fs'; -import os from 'os'; -import { RequiredTemplateValues } from '../stages/templater'; -import { makeLogStream } from './logger'; -import { JobProcessor } from './processor'; -import { StageInput } from './types'; - -describe('JobProcessor', () => { - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'example@email.com', - }, - }; - - const mockValues: RequiredTemplateValues = { - owner: 'blobby', - storePath: 'https://github.com/backstage/mock-repo', - destination: { - git: parseGitUrl('https://github.com/backstage/mock-repo'), - }, - }; - - const workingDirectory = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - - // NOTE(freben): Without this line, mock-fs makes winston/logform break. - // There are a number of reported issues with logform and its use of dynamic - // strings for imports. It confuses webpack. The basic fix is to trigger - // those imports before mock-fs runs. I wanted to add a mock dir - // 'node_modules': mockFs.passthrough(), but that doesn't seem to be a thing - // in mock-fs 4. - // Probable REAL fix: https://github.com/winstonjs/logform/pull/117 - makeLogStream({}); - - beforeEach(() => { - mockFs({ - [workingDirectory]: mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - describe('create', () => { - it('creates should create a new job with a unique id', async () => { - const processor = new JobProcessor(workingDirectory); - - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages: [], - }); - - expect(job.id).toMatch( - /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, - ); - }); - - it('should setup the correct context for the job', async () => { - const processor = new JobProcessor(workingDirectory); - - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages: [], - }); - - expect(job.context.entity).toBe(mockEntity); - expect(job.context.values).toBe(mockValues); - }); - - it('should set the status as pending', async () => { - const processor = new JobProcessor(workingDirectory); - - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages: [], - }); - - expect(job.status).toBe('PENDING'); - }); - - it('should create the correct stages', async () => { - const stages: StageInput[] = [ - { - name: 'Do something cool step 1', - handler: jest.fn(), - }, - { - name: 'Do something cool step 2', - handler: jest.fn(), - }, - ]; - - const processor = new JobProcessor(workingDirectory); - - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages, - }); - - expect(job.stages).toHaveLength(stages.length); - - for (let i = 0; i < job.stages.length; i++) { - expect(job.stages[i].name).toBe(stages[i].name); - expect(job.stages[i].status).toBe('PENDING'); - } - }); - }); - - describe('get', () => { - it('return undefined for when the job does not exist', () => { - const processor = new JobProcessor(workingDirectory); - expect(processor.get('123')).not.toBeDefined(); - }); - - it('should return the exact same instance of the job when one is created', async () => { - const processor = new JobProcessor(workingDirectory); - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages: [], - }); - - expect(processor.get(job.id)).toBe(job); - }); - }); - - describe('process', () => { - it('throws an error when the status of the job is not in pending state', async () => { - const processor = new JobProcessor(workingDirectory); - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages: [], - }); - - job.status = 'STARTED'; - - await expect(processor.run(job)).rejects.toThrow( - /Job is not in a 'PENDING' state/, - ); - }); - - it('will call each of the handlers in the stages', async () => { - const stages: StageInput[] = [ - { - name: 'c/o', - handler: jest.fn(), - }, - { - name: 'g/p', - handler: jest.fn(), - }, - ]; - - const processor = new JobProcessor(workingDirectory); - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages, - }); - - await processor.run(job); - - for (const stage of stages) { - expect(stage.handler).toHaveBeenCalled(); - } - }); - - it('should set all stages to complete and the job to complete when finishes without errors', async () => { - const stages: StageInput[] = [ - { - name: 'c/o', - handler: jest.fn(), - }, - { - name: 'g/p', - handler: jest.fn(), - }, - ]; - - const processor = new JobProcessor(workingDirectory); - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages, - }); - - await processor.run(job); - - for (const stage of job.stages) { - expect(stage.status).toBe('COMPLETED'); - } - - expect(job.status).toBe('COMPLETED'); - }); - - it('should merge the return value from previous steps into the context of the next step', async () => { - const stages: StageInput[] = [ - { - name: 'c/o', - handler: jest - .fn() - .mockResolvedValue({ first: 'ben', second: 'lambert' }), - }, - { - name: 'g/p', - handler: jest - .fn() - .mockResolvedValue({ second: 'linus', third: 'lambert' }), - }, - { - name: 'go', - handler: jest.fn(), - }, - ]; - - const processor = new JobProcessor(workingDirectory); - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages, - }); - - await processor.run(job); - - expect(stages[1].handler).toHaveBeenCalledWith( - expect.objectContaining({ first: 'ben', second: 'lambert' }), - ); - - expect(stages[2].handler).toHaveBeenCalledWith( - expect.objectContaining({ - first: 'ben', - second: 'linus', - third: 'lambert', - }), - ); - }); - - it('should fail the job and the step if one of them fails', async () => { - const fail = new Error('something went wrong here'); - const stages: StageInput[] = [ - { - name: 'c/o', - handler: jest.fn(), - }, - { - name: 'g/p', - handler: jest.fn().mockRejectedValue(fail), - }, - { - name: 'go', - handler: jest.fn(), - }, - ]; - - const processor = new JobProcessor(workingDirectory); - const job = processor.create({ - entity: mockEntity, - values: mockValues, - stages, - }); - - await processor.run(job); - - expect(job.status).toBe('FAILED'); - expect(job.stages[0].status).toBe('COMPLETED'); - expect(job.stages[1].status).toBe('FAILED'); - expect(job.stages[2].status).toBe('PENDING'); - expect(job.error?.message).toBe('something went wrong here'); - expect(job.stages[1].log.join()).toContain('something went wrong here'); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts deleted file mode 100644 index ef603477c1..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import os from 'os'; -import fs from 'fs-extra'; -import { Processor, Job, StageContext, StageInput } from './types'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import * as uuid from 'uuid'; -import path from 'path'; -import { TemplaterValues } from '../stages/templater'; -import { makeLogStream } from './logger'; -import { Logger } from 'winston'; -import { Config } from '@backstage/config'; - -export type JobAndDirectoryTuple = { - job: Job; - directory: string; -}; - -export class JobProcessor implements Processor { - private readonly workingDirectory: string; - private readonly jobs: Map; - - static async fromConfig({ - config, - logger, - }: { - config: Config; - logger: Logger; - }) { - let workingDirectory: string; - if (config.has('backend.workingDirectory')) { - workingDirectory = config.getString('backend.workingDirectory'); - try { - // Check if working directory exists and is writable - await fs.promises.access( - workingDirectory, - fs.constants.F_OK | fs.constants.W_OK, - ); - logger.info(`using working directory: ${workingDirectory}`); - } catch (err) { - logger.error( - `working directory ${workingDirectory} ${ - err.code === 'ENOENT' ? 'does not exist' : 'is not writable' - }`, - ); - throw err; - } - } else { - workingDirectory = os.tmpdir(); - } - - return new JobProcessor(workingDirectory); - } - - constructor(workingDirectory: string) { - this.workingDirectory = workingDirectory; - this.jobs = new Map(); - } - - create({ - entity, - values, - stages, - }: { - entity: TemplateEntityV1alpha1; - values: TemplaterValues; - stages: StageInput[]; - }): Job { - const id = uuid.v4(); - const { logger, stream } = makeLogStream({ id }); - - const context: StageContext = { - entity, - values, - logger, - logStream: stream, - workspacePath: path.join(this.workingDirectory, id), - }; - - const job: Job = { - id, - context, - stages: stages.map(stage => ({ - handler: stage.handler, - log: [], - name: stage.name, - status: 'PENDING', - })), - status: 'PENDING', - }; - - this.jobs.set(job.id, job); - - return job; - } - - get(id: string): Job | undefined { - return this.jobs.get(id); - } - - async run(job: Job): Promise { - if (job.status !== 'PENDING') { - throw new Error("Job is not in a 'PENDING' state"); - } - - await fs.mkdir(job.context.workspacePath); - - job.status = 'STARTED'; - - try { - for (const stage of job.stages) { - // Create a logger for each stage so we can create separate - // Streams for each step. - const { logger, log, stream } = makeLogStream({ - id: job.id, - stage: stage.name, - }); - // Attach the logger to the stage, and setup some timestamps. - stage.log = log; - stage.startedAt = Date.now(); - - try { - // Run the handler with the context created for the Job and some - // Additional logging helpers. - stage.status = 'STARTED'; - const handlerResponse = await stage.handler({ - ...job.context, - logger, - logStream: stream, - }); - - // If the handler returns something, then let's merge this onto the - // context for the next stage to use as it might be relevant. - if (handlerResponse) { - job.context = { - ...job.context, - ...handlerResponse, - }; - } - - // Complete the current stage - stage.status = 'COMPLETED'; - } catch (error) { - // Log to the current stage the error that occurred and fail the stage. - stage.status = 'FAILED'; - logger.error(`Stage failed with error: ${error.message}`); - logger.debug(error.stack); - // Throw the error so the job can be failed too. - throw error; - } finally { - // Always set the stage end timestamp. - stage.endedAt = Date.now(); - } - } - - // If all went to plan, complete the job. - job.status = 'COMPLETED'; - } catch (error) { - // If something went wrong, fail the job, and set the error property on the job. - job.error = { name: error.name, message: error.message }; - job.status = 'FAILED'; - } finally { - await fs.remove(job.context.workspacePath); - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/types.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/types.ts deleted file mode 100644 index 84509a6c0f..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import type { Writable } from 'stream'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { TemplaterValues } from '../stages/templater'; -import { Logger } from 'winston'; - -// Context will be a mutable object which is passed between stages -// To share data, but also thinking that we can pass in functions here too -// To maybe create sub steps or fail the entire thing, or skip stages down the line. -export type StageContext = { - values: TemplaterValues; - entity: TemplateEntityV1alpha1; - logger: Logger; - logStream: Writable; - workspacePath: string; -} & T; - -export type ProcessorStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; - -export interface StageResult extends StageInput { - log: string[]; - status: ProcessorStatus; - startedAt?: number; - endedAt?: number; -} - -export interface StageInput { - name: string; - handler(ctx: StageContext): Promise; -} - -export type Job = { - id: string; - context: StageContext; - status: ProcessorStatus; - stages: StageResult[]; - error?: Error; -}; - -export type Processor = { - create({ - entity, - values, - stages, - }: { - entity: TemplateEntityV1alpha1; - values: TemplaterValues; - stages: StageInput[]; - }): Job; - - get(id: string): Job | undefined; - - run(job: Job): Promise; -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts deleted file mode 100644 index 8f21485f64..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - LOCATION_ANNOTATION, - TemplateEntityV1alpha1, -} from '@backstage/catalog-model'; -import { joinGitUrlPath, parseLocationAnnotation } from './helpers'; - -describe('Helpers', () => { - describe('parseLocationAnnotation', () => { - it('throws an exception when no annotation location', () => { - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: {}, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'team-d@example.com', - }, - }; - - expect(() => parseLocationAnnotation(mockEntity)).toThrow( - expect.objectContaining({ - name: 'InputError', - message: `No location annotation provided in entity: ${mockEntity.metadata.name}`, - }), - ); - }); - - it('should throw an error when the protocol part is not set in the location annotation', () => { - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - [LOCATION_ANNOTATION]: - ':https://github.com/o/r/blob/master/template.yaml', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'team-b@example.com', - }, - }; - - expect(() => parseLocationAnnotation(mockEntity)).toThrow( - expect.objectContaining({ - name: 'TypeError', - message: - "Unable to parse location reference ':https://github.com/o/r/blob/master/template.yaml', expected ':', e.g. 'url:https://host/path'", - }), - ); - }); - - it('should throw an error when the location part is not set in the location annotation', () => { - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - [LOCATION_ANNOTATION]: 'github:', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'team-a@example.com', - }, - }; - - expect(() => parseLocationAnnotation(mockEntity)).toThrow( - expect.objectContaining({ - name: 'TypeError', - message: `Unable to parse location reference 'github:', expected ':', e.g. 'url:https://host/path'`, - }), - ); - }); - - it('should parse the location and protocol correctly for simple locations', () => { - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - [LOCATION_ANNOTATION]: 'file:./path', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'team-b@example.com', - }, - }; - - expect(parseLocationAnnotation(mockEntity)).toEqual({ - protocol: 'file', - location: './path', - }); - }); - - it('should parse the location and protocol correctly for complex with unescaped locations', () => { - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - [LOCATION_ANNOTATION]: 'github:https://lol.com/:something/shello', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - owner: 'team-c@example.com', - }, - }; - - expect(parseLocationAnnotation(mockEntity)).toEqual({ - protocol: 'github', - location: 'https://lol.com/:something/shello', - }); - }); - }); - - describe('joinGitUrlPath', () => { - it.each([ - [ - 'https://github.com/o/r/blob/master/template.yaml', - 'template', - 'https://github.com/o/r/blob/master/template', - ], - [ - 'https://dev.azure.com/o/p/_git/template-repo?path=%2Ftemplate.yaml', - undefined, - 'https://dev.azure.com/o/p/_git/template-repo?path=%2F', - ], - [ - 'https://dev.azure.com/o/p/_git/template-repo?path=%2Ftemplate.yaml', - 'a', - 'https://dev.azure.com/o/p/_git/template-repo?path=%2Fa', - ], - [ - 'https://dev.azure.com/o/p/_git/template-repo?path=%2Fa%2Ftemplate.yaml', - 'b', - 'https://dev.azure.com/o/p/_git/template-repo?path=%2Fa%2Fb', - ], - [ - 'https://github.com/o/r/blob/master/template.yaml', - undefined, - 'https://github.com/o/r/blob/master', - ], - [ - 'https://github.com/o/r/blob/master/template.yaml', - 'template', - 'https://github.com/o/r/blob/master/template', - ], - [ - 'https://github.com/o/r/blob/master/templates/graphql-starter/template.yaml', - 'template', - 'https://github.com/o/r/blob/master/templates/graphql-starter/template', - ], - [ - 'https://gitlab.com/o/r/-/blob/master/template.yaml', - undefined, - 'https://gitlab.com/o/r/-/blob/master', - ], - [ - 'https://gitlab.com/o/r/-/blob/master/template.yaml', - 'template', - 'https://gitlab.com/o/r/-/blob/master/template', - ], - [ - 'https://gitlab.com/o/r/-/blob/master/a/b/c/template.yaml', - '../../c', - 'https://gitlab.com/o/r/-/blob/master/a/c', - ], - [ - 'https://bitbucket.org/p/r/src/master/a/b/template.yaml', - undefined, - 'https://bitbucket.org/p/r/src/master/a/b', - ], - [ - 'https://bitbucket.org/p/r/src/master/a/b/template.yaml', - 'c', - 'https://bitbucket.org/p/r/src/master/a/b/c', - ], - [ - 'https://bitbucket.org/p/r/src/master/a/b/template.yaml', - '../c', - 'https://bitbucket.org/p/r/src/master/a/c', - ], - ])('should join git url %s with path %s', (url, path, result) => { - expect(joinGitUrlPath(url, path)).toBe(result); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts deleted file mode 100644 index 8085277030..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { InputError } from '@backstage/errors'; -import { - LOCATION_ANNOTATION, - parseLocationReference, - TemplateEntityV1alpha1, -} from '@backstage/catalog-model'; -import { posix as posixPath } from 'path'; - -export type ParsedLocationAnnotation = { - protocol: 'file' | 'url'; - location: string; -}; - -export const parseLocationAnnotation = ( - entity: TemplateEntityV1alpha1, -): ParsedLocationAnnotation => { - const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - if (!annotation) { - throw new InputError( - `No location annotation provided in entity: ${entity.metadata.name}`, - ); - } - - const { type, target } = parseLocationReference(annotation); - return { - protocol: type as 'file' | 'url', - location: target, - }; -}; - -export function joinGitUrlPath(repoUrl: string, path?: string): string { - const parsed = new URL(repoUrl); - - if (parsed.hostname.endsWith('azure.com')) { - const templatePath = posixPath.normalize( - posixPath.join( - posixPath.dirname(parsed.searchParams.get('path') || '/'), - path || '.', - ), - ); - parsed.searchParams.set('path', templatePath); - return parsed.toString(); - } - - return new URL(path || '.', repoUrl).toString().replace(/\/$/, ''); -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/index.ts deleted file mode 100644 index c6f824a139..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from './prepare'; -export * from './publish'; -export * from './templater'; -export * from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts deleted file mode 100644 index efbb241470..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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. - */ - -import { createTemplateAction } from '../actions'; -import { FilePreparer, PreparerBuilder } from './prepare'; -import { PublisherBuilder } from './publish'; -import { TemplaterBuilder, TemplaterValues } from './templater'; - -type Options = { - preparers: PreparerBuilder; - templaters: TemplaterBuilder; - publishers: PublisherBuilder; -}; - -export function createLegacyActions(options: Options) { - const { preparers, templaters, publishers } = options; - - return [ - createTemplateAction({ - id: 'legacy:prepare', - async handler(ctx) { - ctx.logger.info('Preparing the skeleton'); - const { protocol, url } = ctx.input; - const preparer = - protocol === 'file' - ? new FilePreparer() - : preparers.get(url as string); - - await preparer.prepare({ - url: url as string, - logger: ctx.logger, - workspacePath: ctx.workspacePath, - }); - }, - }), - createTemplateAction({ - id: 'legacy:template', - async handler(ctx) { - ctx.logger.info('Running the templater'); - const templater = templaters.get(ctx.input.templater as string); - await templater.run({ - workspacePath: ctx.workspacePath, - logStream: ctx.logStream, - values: ctx.input.values as TemplaterValues, - }); - }, - }), - createTemplateAction({ - id: 'legacy:publish', - async handler(ctx) { - const { values } = ctx.input; - if ( - typeof values !== 'object' || - values === null || - Array.isArray(values) - ) { - throw new Error( - `Invalid values passed to publish, got ${typeof values}`, - ); - } - const storePath = values.storePath as unknown; - if (typeof storePath !== 'string') { - throw new Error( - `Invalid store path passed to publish, got ${typeof storePath}`, - ); - } - const owner = values.owner as unknown; - if (typeof owner !== 'string') { - throw new Error( - `Invalid owner passed to publish, got ${typeof owner}`, - ); - } - - const publisher = publishers.get(storePath); - ctx.logger.info('Will now store the template'); - const { remoteUrl, catalogInfoUrl } = await publisher.publish({ - values: { - ...values, - owner, - storePath, - }, - workspacePath: ctx.workspacePath, - logger: ctx.logger, - }); - ctx.output('remoteUrl', remoteUrl); - if (catalogInfoUrl) { - ctx.output('catalogInfoUrl', catalogInfoUrl); - } - }, - }), - ]; -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts deleted file mode 100644 index 74a21e0833..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import os from 'os'; -import path from 'path'; -import { AzurePreparer } from './azure'; -import { getVoidLogger, Git } from '@backstage/backend-common'; - -jest.mock('fs-extra'); - -describe('AzurePreparer', () => { - const mockGitClient = { - clone: jest.fn(), - }; - - const logger = getVoidLogger(); - - jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); - - const preparer = AzurePreparer.fromConfig({ - host: 'dev.azure.com', - token: 'fake-azure-token', - }); - - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = path.resolve(workspacePath, 'checkout'); - const templatePath = path.resolve(workspacePath, 'template'); - const prepareOptions = { - url: - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - workspacePath, - logger, - }; - - it('calls the clone command with token from integrations config', async () => { - await preparer.prepare(prepareOptions); - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - password: 'fake-azure-token', - username: 'notempty', - }); - expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); - }); - - it('calls the clone command with the correct arguments for a repository', async () => { - await preparer.prepare(prepareOptions); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - dir: checkoutPath, - }); - }); - - it('calls the clone command with the correct arguments for a repository with a specified branch', async () => { - await preparer.prepare({ - url: - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml&version=GBmaster', - logger, - workspacePath, - }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - dir: checkoutPath, - ref: 'master', - }); - }); - - it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - await preparer.prepare({ - url: - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - workspacePath, - logger, - }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - dir: checkoutPath, - }); - expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - }); - - it('moves the template from path if it is specified', async () => { - await preparer.prepare({ - url: `https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=${encodeURIComponent( - './subdir', - )}`, - logger, - workspacePath, - }); - - expect(fs.move).toHaveBeenCalledWith( - path.resolve(checkoutPath, 'subdir'), - templatePath, - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts deleted file mode 100644 index a405288f9f..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import fs from 'fs-extra'; -import path from 'path'; -import { Git } from '@backstage/backend-common'; -import { PreparerBase, PreparerOptions } from './types'; -import parseGitUrl from 'git-url-parse'; -import { AzureIntegrationConfig } from '@backstage/integration'; - -export class AzurePreparer implements PreparerBase { - static fromConfig(config: AzureIntegrationConfig) { - return new AzurePreparer({ token: config.token }); - } - - constructor(private readonly config: { token?: string }) {} - - async prepare({ url, workspacePath, logger }: PreparerOptions) { - const parsedGitUrl = parseGitUrl(url); - const checkoutPath = path.join(workspacePath, 'checkout'); - const targetPath = path.join(workspacePath, 'template'); - const fullPathToTemplate = path.resolve( - checkoutPath, - parsedGitUrl.filepath ?? '', - ); - - // Username can be anything but the empty string according to: - // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - const git = this.config.token - ? Git.fromAuth({ - password: this.config.token, - username: 'notempty', - logger, - }) - : Git.fromAuth({ logger }); - - await git.clone({ - url: parsedGitUrl.toString('https'), - ref: parsedGitUrl.ref, - dir: checkoutPath, - }); - - await fs.move(fullPathToTemplate, targetPath); - - try { - await fs.rmdir(path.join(targetPath, '.git')); - } catch { - // Ignore intentionally - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts deleted file mode 100644 index 2c4e24fed0..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import { BitbucketPreparer } from './bitbucket'; -import { getVoidLogger, Git } from '@backstage/backend-common'; -import path from 'path'; -import os from 'os'; - -jest.mock('fs-extra'); - -describe('BitbucketPreparer', () => { - const logger = getVoidLogger(); - const mockGitClient = { - clone: jest.fn(), - }; - - jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - const preparer = BitbucketPreparer.fromConfig({ - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-password', - }); - - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = path.resolve(workspacePath, 'checkout'); - const templatePath = path.resolve(workspacePath, 'template'); - - const prepareOptions = { - url: 'https://bitbucket.org/backstage-project/backstage-repo', - logger, - workspacePath, - }; - - it('calls the clone command with the correct arguments for a repository', async () => { - await preparer.prepare(prepareOptions); - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://bitbucket.org/backstage-project/backstage-repo', - dir: checkoutPath, - ref: expect.any(String), - }); - expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); - }); - - it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => { - const preparerCheck = BitbucketPreparer.fromConfig({ - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-password', - }); - await preparerCheck.prepare(prepareOptions); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'fake-user', - password: 'fake-password', - }); - }); - - it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - await preparer.prepare(prepareOptions); - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://bitbucket.org/backstage-project/backstage-repo', - dir: checkoutPath, - ref: expect.any(String), - }); - }); - - it('moves a template subdirectory to checkout if specified', async () => { - await preparer.prepare({ - url: 'https://bitbucket.org/foo/bar/src/master/1/2/3', - logger, - workspacePath, - }); - expect(fs.move).toHaveBeenCalledWith( - path.resolve(checkoutPath, '1', '2', '3'), - templatePath, - ); - }); - - it('calls the clone command with with token for auth method', async () => { - const preparerCheck = BitbucketPreparer.fromConfig({ - host: 'bitbucket.org', - token: 'fake-token', - }); - await preparerCheck.prepare(prepareOptions); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'x-token-auth', - password: 'fake-token', - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts deleted file mode 100644 index 36f6b07750..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import fs from 'fs-extra'; -import path from 'path'; -import { Git } from '@backstage/backend-common'; -import { PreparerBase, PreparerOptions } from './types'; -import { BitbucketIntegrationConfig } from '@backstage/integration'; -import parseGitUrl from 'git-url-parse'; - -export class BitbucketPreparer implements PreparerBase { - static fromConfig(config: BitbucketIntegrationConfig) { - return new BitbucketPreparer({ - username: config.username, - token: config.token, - appPassword: config.appPassword, - }); - } - - constructor( - private readonly config: { - username?: string; - token?: string; - appPassword?: string; - }, - ) {} - - async prepare({ url, workspacePath, logger }: PreparerOptions) { - const parsedGitUrl = parseGitUrl(url); - const checkoutPath = path.join(workspacePath, 'checkout'); - const targetPath = path.join(workspacePath, 'template'); - const fullPathToTemplate = path.resolve( - checkoutPath, - parsedGitUrl.filepath ?? '', - ); - - const git = Git.fromAuth({ logger, ...this.getAuth() }); - - await git.clone({ - url: parsedGitUrl.toString('https'), - dir: checkoutPath, - ref: parsedGitUrl.ref, - }); - - await fs.move(fullPathToTemplate, targetPath); - - try { - await fs.rmdir(path.join(targetPath, '.git')); - } catch { - // Ignore intentionally - } - } - - private getAuth(): { username: string; password: string } | undefined { - const { username, token, appPassword } = this.config; - - if (username && appPassword) { - return { username: username, password: appPassword }; - } - - if (token) { - return { - username: 'x-token-auth', - password: token! || appPassword!, - }; - } - - return undefined; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts deleted file mode 100644 index 73d944db67..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getVoidLogger } from '@backstage/backend-common'; -import fs from 'fs-extra'; -import { FilePreparer } from './file'; -import os from 'os'; -import path from 'path'; - -jest.mock('fs-extra'); - -describe('File preparer', () => { - it('prepares templates from a file path', async () => { - const logger = getVoidLogger(); - const preparer = new FilePreparer(); - const root = os.platform() === 'win32' ? 'C:\\' : '/'; - const workspacePath = path.join(root, 'tmp'); - const targetPath = path.resolve(workspacePath, 'template'); - - await preparer.prepare({ - url: `file://${root}path/to/template`, - logger, - workspacePath, - }); - expect(fs.copy).toHaveBeenCalledWith( - path.join(root, 'path', 'to', 'template'), - targetPath, - { - recursive: true, - }, - ); - expect(fs.ensureDir).toHaveBeenCalledWith(targetPath); - - await expect( - preparer.prepare({ - url: 'http://not/file/path', - logger, - workspacePath, - }), - ).rejects.toThrow( - "Wrong location protocol, should be 'file', http://not/file/path", - ); - - if (os.platform() === 'win32') { - // eslint-disable-next-line jest/no-conditional-expect - await expect( - preparer.prepare({ - url: 'file:///unix/file/path', - logger, - workspacePath, - }), - ).rejects.toThrow('File URL path must be absolute'); - } else { - // eslint-disable-next-line jest/no-conditional-expect - await expect( - preparer.prepare({ - url: 'file://not/full/path', - logger, - workspacePath, - }), - ).rejects.toThrow( - `File URL host must be "localhost" or empty on ${os.platform()}`, - ); - } - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts deleted file mode 100644 index 92ee93511c..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import fs from 'fs-extra'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { InputError } from '@backstage/errors'; -import { PreparerBase, PreparerOptions } from './types'; - -export class FilePreparer implements PreparerBase { - async prepare({ url, workspacePath }: PreparerOptions) { - if (!url.startsWith('file://')) { - throw new InputError(`Wrong location protocol, should be 'file', ${url}`); - } - - const templatePath = fileURLToPath(url); - - const targetDir = path.join(workspacePath, 'template'); - await fs.ensureDir(targetDir); - - await fs.copy(templatePath, targetDir, { - recursive: true, - }); - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts deleted file mode 100644 index 0438e28f01..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import os from 'os'; -import path from 'path'; -import { GithubPreparer } from './github'; -import { getVoidLogger, Git } from '@backstage/backend-common'; - -jest.mock('fs-extra'); - -describe('GitHubPreparer', () => { - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = path.resolve(workspacePath, 'checkout'); - const templatePath = path.resolve(workspacePath, 'template'); - - const mockGitClient = { - clone: jest.fn(), - }; - const logger = getVoidLogger(); - - jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - const preparer = GithubPreparer.fromConfig({ - host: 'github.com', - token: 'fake-token', - }); - - it('calls the clone command with the correct arguments for a repository', async () => { - await preparer.prepare({ - url: - 'https://github.com/benjdlambert/backstage-graphql-template/blob/master/templates/graphql-starter/template', - logger, - workspacePath, - }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://github.com/benjdlambert/backstage-graphql-template', - dir: checkoutPath, - ref: expect.any(String), - }); - expect(fs.move).toHaveBeenCalledWith( - path.resolve(checkoutPath, 'templates', 'graphql-starter', 'template'), - templatePath, - ); - expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); - }); - - it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - await preparer.prepare({ - url: - 'https://github.com/benjdlambert/backstage-graphql-template/blob/master', - logger, - workspacePath, - }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://github.com/benjdlambert/backstage-graphql-template', - dir: checkoutPath, - ref: 'master', - }); - expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); - }); - - it('calls the clone command with token', async () => { - await preparer.prepare({ - url: - 'https://github.com/benjdlambert/backstage-graphql-template/blob/master', - logger, - workspacePath, - }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'x-access-token', - password: 'fake-token', - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts deleted file mode 100644 index dec7fc06c2..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import fs from 'fs-extra'; -import path from 'path'; -import { Git } from '@backstage/backend-common'; -import { PreparerBase, PreparerOptions } from './types'; -import parseGitUrl from 'git-url-parse'; -import { - GitHubIntegrationConfig, - GithubCredentialsProvider, -} from '@backstage/integration'; - -export class GithubPreparer implements PreparerBase { - static fromConfig(config: GitHubIntegrationConfig) { - const credentialsProvider = GithubCredentialsProvider.create(config); - return new GithubPreparer({ credentialsProvider }); - } - - constructor( - private readonly config: { credentialsProvider: GithubCredentialsProvider }, - ) {} - - async prepare({ url, workspacePath, logger }: PreparerOptions) { - const parsedGitUrl = parseGitUrl(url); - const checkoutPath = path.join(workspacePath, 'checkout'); - const targetPath = path.join(workspacePath, 'template'); - const fullPathToTemplate = path.resolve( - checkoutPath, - parsedGitUrl.filepath ?? '', - ); - - const { token } = await this.config.credentialsProvider.getCredentials({ - url, - }); - - const git = token - ? Git.fromAuth({ - username: 'x-access-token', - password: token, - logger, - }) - : Git.fromAuth({ logger }); - - await git.clone({ - url: parsedGitUrl.toString('https'), - dir: checkoutPath, - ref: parsedGitUrl.ref, - }); - - await fs.move(fullPathToTemplate, targetPath); - - try { - await fs.rmdir(path.join(targetPath, '.git')); - } catch { - // Ignore intentionally - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts deleted file mode 100644 index fa76c74bd7..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import fs from 'fs-extra'; -import os from 'os'; -import path from 'path'; -import { GitlabPreparer } from './gitlab'; -import { getVoidLogger, Git } from '@backstage/backend-common'; - -jest.mock('fs-extra'); - -describe('GitLabPreparer', () => { - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = path.resolve(workspacePath, 'checkout'); - const templatePath = path.resolve(workspacePath, 'template'); - - const mockGitClient = { - clone: jest.fn(), - }; - const logger = getVoidLogger(); - - jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); - - beforeEach(() => { - jest.clearAllMocks(); - }); - const preparer = GitlabPreparer.fromConfig({ - host: '', - token: 'fake-token', - apiBaseUrl: '', - baseUrl: '', - }); - - it(`calls the clone command with the correct arguments for a repository`, async () => { - await preparer.prepare({ - url: - 'https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master', - logger, - workspacePath, - }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template.git', - dir: checkoutPath, - ref: expect.any(String), - }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - - expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); - }); - - it(`clones the template from a sub directory if specified`, async () => { - await preparer.prepare({ - url: - 'https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/1/2/3', - logger, - workspacePath, - }); - expect(fs.move).toHaveBeenCalledWith( - path.resolve(checkoutPath, '1', '2', '3'), - templatePath, - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts deleted file mode 100644 index 582214606b..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Git } from '@backstage/backend-common'; -import { GitLabIntegrationConfig } from '@backstage/integration'; -import fs from 'fs-extra'; -import parseGitUrl from 'git-url-parse'; -import path from 'path'; -import { PreparerBase, PreparerOptions } from './types'; - -export class GitlabPreparer implements PreparerBase { - static fromConfig(config: GitLabIntegrationConfig) { - return new GitlabPreparer({ token: config.token }); - } - - constructor(private readonly config: { token?: string }) {} - - async prepare({ url, workspacePath, logger }: PreparerOptions) { - const parsedGitUrl = parseGitUrl(url); - const checkoutPath = path.join(workspacePath, 'checkout'); - const targetPath = path.join(workspacePath, 'template'); - const fullPathToTemplate = path.resolve( - checkoutPath, - parsedGitUrl.filepath ?? '', - ); - parsedGitUrl.git_suffix = true; - - const git = this.config.token - ? Git.fromAuth({ - password: this.config.token, - username: 'oauth2', - logger, - }) - : Git.fromAuth({ logger }); - - await git.clone({ - url: parsedGitUrl.toString('https'), - dir: checkoutPath, - ref: parsedGitUrl.ref, - }); - - await fs.move(fullPathToTemplate, targetPath); - - try { - await fs.rmdir(path.join(targetPath, '.git')); - } catch { - // Ignore intentionally - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts deleted file mode 100644 index 060b99b3a8..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { AzurePreparer } from './azure'; -export { BitbucketPreparer } from './bitbucket'; -export { FilePreparer } from './file'; -export { GithubPreparer } from './github'; -export { GitlabPreparer } from './gitlab'; -export { Preparers } from './preparers'; -export type { PreparerBase, PreparerBuilder, PreparerOptions } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts deleted file mode 100644 index 20dfad924b..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GithubPreparer } from './github'; -import { Preparers } from './preparers'; - -describe('Preparers', () => { - it('should return the correct preparer based on the hostname', async () => { - const preparer = await GithubPreparer.fromConfig({ - host: 'github.com', - apiBaseUrl: 'lols', - token: 'something else yo', - }); - - const preparers = new Preparers(); - preparers.register('github.com', preparer); - - expect( - preparers.get('https://github.com/please/find/me/something/from/github'), - ).toBe(preparer); - }); - - it('should throw an error if there is nothing that will match the url provided', async () => { - const preparer = await GithubPreparer.fromConfig({ - host: 'github.com', - apiBaseUrl: 'lols', - token: 'something else yo', - }); - - const preparers = new Preparers(); - preparers.register('github.com', preparer); - - expect(() => preparers.get('https://404.com')).toThrow( - `Unable to find a preparer for URL: https://404.com. Please make sure to register this host under an integration in app-config`, - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts deleted file mode 100644 index 98ca984c17..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; -import { PreparerBase, PreparerBuilder } from './types'; -import { Logger } from 'winston'; - -import { GitlabPreparer } from './gitlab'; -import { AzurePreparer } from './azure'; -import { GithubPreparer } from './github'; -import { BitbucketPreparer } from './bitbucket'; -import { ScmIntegrations } from '@backstage/integration'; - -export class Preparers implements PreparerBuilder { - private preparerMap = new Map(); - - register(host: string, preparer: PreparerBase) { - this.preparerMap.set(host, preparer); - } - - get(url: string): PreparerBase { - const preparer = this.preparerMap.get(new URL(url).host); - if (!preparer) { - throw new Error( - `Unable to find a preparer for URL: ${url}. Please make sure to register this host under an integration in app-config`, - ); - } - return preparer; - } - - static async fromConfig( - config: Config, - // eslint-disable-next-line - _: { logger: Logger }, - ): Promise { - const preparers = new Preparers(); - const scm = ScmIntegrations.fromConfig(config); - for (const integration of scm.azure.list()) { - preparers.register( - integration.config.host, - AzurePreparer.fromConfig(integration.config), - ); - } - - for (const integration of scm.github.list()) { - preparers.register( - integration.config.host, - GithubPreparer.fromConfig(integration.config), - ); - } - - for (const integration of scm.gitlab.list()) { - preparers.register( - integration.config.host, - GitlabPreparer.fromConfig(integration.config), - ); - } - - for (const integration of scm.bitbucket.list()) { - preparers.register( - integration.config.host, - BitbucketPreparer.fromConfig(integration.config), - ); - } - - return preparers; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts deleted file mode 100644 index 1b6b4380f7..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Logger } from 'winston'; - -export type PreparerOptions = { - /** - * Full URL to the directory containg template data - */ - url: string; - /** - * The workspace path that will eventually be the the root of the new repo - */ - workspacePath: string; - logger: Logger; -}; - -export interface PreparerBase { - /** - * Prepare a directory with contents from the remote location - */ - prepare(opts: PreparerOptions): Promise; -} - -export type PreparerBuilder = { - register(host: string, preparer: PreparerBase): void; - get(url: string): PreparerBase; -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts deleted file mode 100644 index 3eecab33b5..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -jest.mock('./helpers'); - -jest.mock('azure-devops-node-api', () => ({ - WebApi: jest.fn(), - getPersonalAccessTokenHandler: jest.fn().mockReturnValue(() => {}), -})); - -import os from 'os'; -import { resolve } from 'path'; -import { AzurePublisher } from './azure'; -import { WebApi } from 'azure-devops-node-api'; -import * as helpers from './helpers'; -import { getVoidLogger } from '@backstage/backend-common'; - -describe('Azure Publisher', () => { - const logger = getVoidLogger(); - - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const resultPath = resolve(workspacePath, 'result'); - - describe('publish: createRemoteInAzure', () => { - it('should use azure-devops-node-api to create a repo in the given project', async () => { - const mockGitClient = { - createRepository: jest.fn(), - }; - const mockGitApi = { - getGitApi: jest.fn().mockReturnValue(mockGitClient), - }; - - ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); - - const publisher = await AzurePublisher.fromConfig({ - host: 'dev.azure.com', - token: 'fake-azure-token', - }); - - mockGitClient.createRepository.mockResolvedValue({ - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - } as { remoteUrl: string }); - - const result = await publisher!.publish({ - values: { - storePath: 'https://dev.azure.com/organisation/project/_git/repo', - owner: 'bob', - }, - workspacePath, - logger, - }); - - expect(WebApi).toHaveBeenCalledWith( - 'https://dev.azure.com/organisation', - expect.any(Function), - ); - - expect(result).toEqual({ - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - catalogInfoUrl: - 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', - }); - expect(mockGitClient.createRepository).toHaveBeenCalledWith( - { - name: 'repo', - }, - 'project', - ); - expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - auth: { username: 'notempty', password: 'fake-azure-token' }, - logger, - }); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts deleted file mode 100644 index 921c139a2e..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { IGitApi } from 'azure-devops-node-api/GitApi'; -import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { initRepoAndPush } from './helpers'; -import { AzureIntegrationConfig } from '@backstage/integration'; -import parseGitUrl from 'git-url-parse'; -import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; -import path from 'path'; - -export class AzurePublisher implements PublisherBase { - static async fromConfig(config: AzureIntegrationConfig) { - if (!config.token) { - return undefined; - } - return new AzurePublisher({ token: config.token }); - } - - constructor(private readonly config: { token: string }) {} - - async publish({ - values, - workspacePath, - logger, - }: PublisherOptions): Promise { - const { owner, name, organization, resource } = parseGitUrl( - values.storePath, - ); - const authHandler = getPersonalAccessTokenHandler(this.config.token); - const webApi = new WebApi( - `https://${resource}/${organization}`, - authHandler, - ); - const client = await webApi.getGitApi(); - - const remoteUrl = await this.createRemote({ - project: owner, - name, - client, - }); - - const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`; - - await initRepoAndPush({ - dir: path.join(workspacePath, 'result'), - remoteUrl, - auth: { - username: 'notempty', - password: this.config.token, - }, - logger, - }); - - return { remoteUrl, catalogInfoUrl }; - } - - private async createRemote(opts: { - name: string; - project: string; - client: IGitApi; - }) { - const { name, project, client } = opts; - const createOptions: GitRepositoryCreateOptions = { name }; - const repo = await client.createRepository(createOptions, project); - - return repo.remoteUrl || ''; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts deleted file mode 100644 index a3f02e2c89..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -jest.mock('./helpers'); - -import os from 'os'; -import { resolve } from 'path'; -import { BitbucketPublisher } from './bitbucket'; -import { initRepoAndPush } from './helpers'; -import { getVoidLogger } from '@backstage/backend-common'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; - -describe('Bitbucket Publisher', () => { - const logger = getVoidLogger(); - const server = setupServer(); - msw.setupDefaultHandlers(server); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const resultPath = resolve(workspacePath, 'result'); - - describe('publish: createRemoteInBitbucketCloud', () => { - it('should create repo in bitbucket cloud', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/project/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/project/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/project/repo', - }, - ], - }, - }), - ), - ), - ); - - const publisher = await BitbucketPublisher.fromConfig( - { - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-token', - }, - { repoVisibility: 'private' }, - ); - - const result = await publisher.publish({ - values: { - storePath: 'https://bitbucket.org/project/repo', - owner: 'bob', - }, - workspacePath, - logger: logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://bitbucket.org/project/repo', - catalogInfoUrl: - 'https://bitbucket.org/project/repo/src/master/catalog-info.yaml', - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://bitbucket.org/project/repo', - auth: { username: 'fake-user', password: 'fake-token' }, - logger: logger, - }); - }); - }); - - describe('publish: createRemoteInBitbucketServer', () => { - it('should throw an error if no username present', async () => { - await expect( - BitbucketPublisher.fromConfig( - { - host: 'bitbucket.mycompany.com', - token: 'fake-token', - }, - { repoVisibility: 'private' }, - ), - ).rejects.toThrow( - 'Bitbucket server requires the username to be set in your config', - ); - }); - it('should create repo in bitbucket server', async () => { - server.use( - rest.post( - 'https://bitbucket.mycompany.com/rest/api/1.0/projects/project/repos', - (_, res, ctx) => - res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/projects/project/repos/repo', - }, - ], - clone: [ - { - name: 'http', - href: 'https://bitbucket.mycompany.com/scm/project/repo', - }, - ], - }, - }), - ), - ), - ); - - const publisher = await BitbucketPublisher.fromConfig( - { - host: 'bitbucket.mycompany.com', - username: 'foo', - token: 'fake-token', - }, - { repoVisibility: 'private' }, - ); - - const result = await publisher.publish({ - values: { - storePath: 'https://bitbucket.mycompany.com/project/repo', - owner: 'bob', - }, - workspacePath, - logger: logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', - catalogInfoUrl: - 'https://bitbucket.mycompany.com/projects/project/repos/repo/catalog-info.yaml', - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', - auth: { username: 'foo', password: 'fake-token' }, - logger: logger, - }); - }); - }); - - it('should use apiBaseUrl to create the repository if it is set', async () => { - server.use( - rest.post( - 'https://bitbucket.mycompany.com/bitbucket/rest/api/1.0/projects/project/repos', - (_, res, ctx) => - res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/bitbucket/projects/project/repos/repo', - }, - ], - clone: [ - { - name: 'http', - href: - 'https://bitbucket.mycompany.com/bitbucket/scm/project/repo', - }, - ], - }, - }), - ), - ), - ); - - const publisher = await BitbucketPublisher.fromConfig( - { - host: 'bitbucket.mycompany.com', - username: 'foo', - token: 'fake-token', - apiBaseUrl: 'https://bitbucket.mycompany.com/bitbucket/rest/api/1.0', - }, - { repoVisibility: 'private' }, - ); - - const result = await publisher.publish({ - values: { - storePath: 'https://bitbucket.mycompany.com/project/repo', - owner: 'bob', - }, - workspacePath, - logger: logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://bitbucket.mycompany.com/bitbucket/scm/project/repo', - catalogInfoUrl: - 'https://bitbucket.mycompany.com/bitbucket/projects/project/repos/repo/catalog-info.yaml', - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts deleted file mode 100644 index a85518eeae..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { initRepoAndPush } from './helpers'; -import fetch from 'cross-fetch'; -import { BitbucketIntegrationConfig } from '@backstage/integration'; -import parseGitUrl from 'git-url-parse'; -import path from 'path'; - -export type RepoVisibilityOptions = 'private' | 'public'; - -// TODO(blam): We should probably start to use a bitbucket client here that we can change -// the baseURL to point at on-prem or public bitbucket versions like we do for -// github and ghe. There's to much logic and not enough types here for us to say that this way is better than using -// a supported bitbucket client if one exists. -export class BitbucketPublisher implements PublisherBase { - static async fromConfig( - config: BitbucketIntegrationConfig, - { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, - ) { - if (config.host !== 'bitbucket.org' && !config.username) - throw new Error( - 'Bitbucket server requires the username to be set in your config', - ); - - return new BitbucketPublisher({ - host: config.host, - token: config.token, - appPassword: config.appPassword, - username: config.username, - apiBaseUrl: config.apiBaseUrl, - repoVisibility, - }); - } - - constructor( - private readonly config: { - host: string; - token?: string; - appPassword?: string; - username?: string; - apiBaseUrl?: string; - repoVisibility: RepoVisibilityOptions; - }, - ) {} - - async publish({ - values, - workspacePath, - logger, - }: PublisherOptions): Promise { - const { owner: project, name } = parseGitUrl(values.storePath); - - const description = values.description as string; - const result = await this.createRemote({ - project, - name, - description, - }); - - await initRepoAndPush({ - dir: path.join(workspacePath, 'result'), - remoteUrl: result.remoteUrl, - auth: { - username: this.config.username ? this.config.username : 'x-token-auth', - password: this.config.appPassword - ? this.config.appPassword - : this.config.token ?? '', - }, - logger, - }); - return result; - } - - private async createRemote(opts: { - project: string; - name: string; - description: string; - }): Promise { - if (this.config.host === 'bitbucket.org') { - return this.createBitbucketCloudRepository(opts); - } - return this.createBitbucketServerRepository(opts); - } - - private async createBitbucketCloudRepository(opts: { - project: string; - name: string; - description: string; - }): Promise { - const { project, name, description } = opts; - - let response: Response; - - const options: RequestInit = { - method: 'POST', - body: JSON.stringify({ - scm: 'git', - description: description, - is_private: this.config.repoVisibility === 'private', - }), - headers: { - Authorization: this.getAuthorizationHeader(), - 'Content-Type': 'application/json', - }, - }; - try { - response = await fetch( - `https://api.bitbucket.org/2.0/repositories/${project}/${name}`, - options, - ); - } catch (e) { - throw new Error(`Unable to create repository, ${e}`); - } - if (response.status === 200) { - const r = await response.json(); - let remoteUrl = ''; - for (const link of r.links.clone) { - if (link.name === 'https') { - remoteUrl = link.href; - } - } - - // TODO use the urlReader to get the default branch - const catalogInfoUrl = `${r.links.html.href}/src/master/catalog-info.yaml`; - return { remoteUrl, catalogInfoUrl }; - } - throw new Error(`Not a valid response code ${await response.text()}`); - } - - private getAuthorizationHeader(): string { - if (this.config.username && this.config.appPassword) { - const buffer = Buffer.from( - `${this.config.username}:${this.config.appPassword}`, - 'utf8', - ); - - return `Basic ${buffer.toString('base64')}`; - } - - if (this.config.token) { - return `Bearer ${this.config.token}`; - } - - throw new Error( - `Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`, - ); - } - - private async createBitbucketServerRepository(opts: { - project: string; - name: string; - description: string; - }): Promise { - const { project, name, description } = opts; - - let response: Response; - const options: RequestInit = { - method: 'POST', - body: JSON.stringify({ - name: name, - description: description, - public: this.config.repoVisibility === 'public', - }), - headers: { - Authorization: this.getAuthorizationHeader(), - 'Content-Type': 'application/json', - }, - }; - - try { - const baseUrl = this.config.apiBaseUrl - ? this.config.apiBaseUrl - : `https://${this.config.host}/rest/api/1.0`; - - response = await fetch(`${baseUrl}/projects/${project}/repos`, options); - } catch (e) { - throw new Error(`Unable to create repository, ${e}`); - } - if (response.status === 201) { - const r = await response.json(); - let remoteUrl = ''; - for (const link of r.links.clone) { - if (link.name === 'http') { - remoteUrl = link.href; - } - } - const catalogInfoUrl = `${r.links.self[0].href}/catalog-info.yaml`; - return { remoteUrl, catalogInfoUrl }; - } - throw new Error(`Not a valid response code ${await response.text()}`); - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts deleted file mode 100644 index fd4256dc86..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -jest.mock('@octokit/rest'); -jest.mock('./helpers'); - -import os from 'os'; -import { resolve } from 'path'; - -import { getVoidLogger } from '@backstage/backend-common'; -import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; -import { GithubPublisher } from './github'; -import { initRepoAndPush } from './helpers'; - -const { mockGithubClient } = require('@octokit/rest') as { - mockGithubClient: { - repos: jest.Mocked; - users: jest.Mocked; - teams: jest.Mocked; - }; -}; - -describe('GitHub Publisher', () => { - const logger = getVoidLogger(); - beforeEach(() => { - jest.clearAllMocks(); - }); - - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const resultPath = resolve(workspacePath, 'result'); - - describe('with public repo visibility', () => { - describe('publish: createRemoteInGithub', () => { - it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { - const publisher = await GithubPublisher.fromConfig( - { - token: 'fake-token', - host: 'github.com', - }, - { repoVisibility: 'public' }, - ); - - mockGithubClient.repos.createInOrg.mockResolvedValue({ - data: { - clone_url: 'https://github.com/backstage/backstage.git', - }, - } as RestEndpointMethodTypes['repos']['createInOrg']['response']); - mockGithubClient.users.getByUsername.mockResolvedValue({ - data: { - type: 'Organization', - }, - } as RestEndpointMethodTypes['users']['getByUsername']['response']); - - const result = await publisher!.publish({ - values: { - storePath: 'https://github.com/blam/test', - owner: 'bob', - access: 'blam/team', - }, - workspacePath, - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://github.com/backstage/backstage.git', - catalogInfoUrl: - 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - }); - expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ - org: 'blam', - name: 'test', - private: false, - visibility: 'public', - }); - expect( - mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg, - ).toHaveBeenCalledWith({ - org: 'blam', - team_slug: 'team', - owner: 'blam', - repo: 'test', - permission: 'admin', - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'x-access-token', password: 'fake-token' }, - logger, - }); - }); - - it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => { - const publisher = await GithubPublisher.fromConfig( - { - token: 'fake-token', - host: 'github.com', - }, - { repoVisibility: 'public' }, - ); - - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - clone_url: 'https://github.com/backstage/backstage.git', - }, - } as RestEndpointMethodTypes['repos']['createForAuthenticatedUser']['response']); - mockGithubClient.users.getByUsername.mockResolvedValue({ - data: { - type: 'User', - }, - } as RestEndpointMethodTypes['users']['getByUsername']['response']); - - const result = await publisher!.publish({ - values: { - storePath: 'https://github.com/blam/test', - owner: 'bob', - access: 'blam', - }, - workspacePath, - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://github.com/backstage/backstage.git', - catalogInfoUrl: - 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - }); - expect( - mockGithubClient.repos.createForAuthenticatedUser, - ).toHaveBeenCalledWith({ - name: 'test', - private: false, - }); - expect(mockGithubClient.repos.addCollaborator).not.toHaveBeenCalled(); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'x-access-token', password: 'fake-token' }, - logger, - }); - }); - }); - - it('should invite other user in the authed user', async () => { - const publisher = await GithubPublisher.fromConfig( - { - token: 'fake-token', - host: 'github.com', - }, - { repoVisibility: 'public' }, - ); - - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - clone_url: 'https://github.com/backstage/backstage.git', - }, - } as RestEndpointMethodTypes['repos']['createForAuthenticatedUser']['response']); - mockGithubClient.users.getByUsername.mockResolvedValue({ - data: { - type: 'User', - }, - } as RestEndpointMethodTypes['users']['getByUsername']['response']); - - const result = await publisher!.publish({ - values: { - storePath: 'https://github.com/blam/test', - owner: 'bob', - access: 'bob', - description: 'description', - }, - workspacePath, - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://github.com/backstage/backstage.git', - catalogInfoUrl: - 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - }); - expect( - mockGithubClient.repos.createForAuthenticatedUser, - ).toHaveBeenCalledWith({ - description: 'description', - name: 'test', - private: false, - }); - expect(mockGithubClient.repos.addCollaborator).toHaveBeenCalledWith({ - owner: 'blam', - repo: 'test', - username: 'bob', - permission: 'admin', - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'x-access-token', password: 'fake-token' }, - logger, - }); - }); - }); - - describe('with internal repo visibility', () => { - it('creates a private repository in the organization with visibility set to internal', async () => { - const publisher = await GithubPublisher.fromConfig( - { - token: 'fake-token', - host: 'github.com', - }, - { repoVisibility: 'internal' }, - ); - - mockGithubClient.repos.createInOrg.mockResolvedValue({ - data: { - clone_url: 'https://github.com/backstage/backstage.git', - }, - } as RestEndpointMethodTypes['repos']['createInOrg']['response']); - mockGithubClient.users.getByUsername.mockResolvedValue({ - data: { - type: 'Organization', - }, - } as RestEndpointMethodTypes['users']['getByUsername']['response']); - - const result = await publisher!.publish({ - values: { - isOrg: true, - storePath: 'https://github.com/blam/test', - owner: 'bob', - }, - workspacePath, - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://github.com/backstage/backstage.git', - catalogInfoUrl: - 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - }); - expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ - org: 'blam', - name: 'test', - private: true, - visibility: 'internal', - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'x-access-token', password: 'fake-token' }, - logger, - }); - }); - }); - - describe('private visibility in a user account', () => { - it('creates a private repository', async () => { - const publisher = await GithubPublisher.fromConfig( - { - token: 'fake-token', - host: 'github.com', - }, - { repoVisibility: 'private' }, - ); - - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - clone_url: 'https://github.com/backstage/backstage.git', - }, - } as RestEndpointMethodTypes['repos']['createForAuthenticatedUser']['response']); - mockGithubClient.users.getByUsername.mockResolvedValue({ - data: { - type: 'User', - }, - } as RestEndpointMethodTypes['users']['getByUsername']['response']); - - const result = await publisher!.publish({ - values: { - storePath: 'https://github.com/blam/test', - owner: 'bob', - }, - workspacePath, - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://github.com/backstage/backstage.git', - catalogInfoUrl: - 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - }); - expect( - mockGithubClient.repos.createForAuthenticatedUser, - ).toHaveBeenCalledWith({ - name: 'test', - private: true, - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'x-access-token', password: 'fake-token' }, - logger, - }); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts deleted file mode 100644 index 9b4b1d48f5..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { - enableBranchProtectionOnDefaultRepoBranch, - initRepoAndPush, -} from './helpers'; -import { - GitHubIntegrationConfig, - GithubCredentialsProvider, -} from '@backstage/integration'; -import parseGitUrl from 'git-url-parse'; -import { Octokit } from '@octokit/rest'; -import path from 'path'; - -export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; - -/** @deprecated use createPublishGithubAction instead */ -export class GithubPublisher implements PublisherBase { - static async fromConfig( - config: GitHubIntegrationConfig, - { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, - ) { - if (!config.token && !config.apps) { - return undefined; - } - - const credentialsProvider = GithubCredentialsProvider.create(config); - - return new GithubPublisher({ - credentialsProvider, - repoVisibility, - apiBaseUrl: config.apiBaseUrl, - }); - } - - constructor( - private readonly config: { - credentialsProvider: GithubCredentialsProvider; - repoVisibility: RepoVisibilityOptions; - apiBaseUrl: string | undefined; - }, - ) {} - - async publish({ - values, - workspacePath, - logger, - }: PublisherOptions): Promise { - const { owner, name } = parseGitUrl(values.storePath); - - const { token } = await this.config.credentialsProvider.getCredentials({ - url: values.storePath, - }); - - if (!token) { - throw new Error( - `No token could be acquired for URL: ${values.storePath}`, - ); - } - - const client = new Octokit({ - auth: token, - baseUrl: this.config.apiBaseUrl, - previews: ['nebula-preview'], - }); - - const description = values.description as string; - const access = values.access as string; - const remoteUrl = await this.createRemote({ - client, - description, - access, - name, - owner, - }); - - await initRepoAndPush({ - dir: path.join(workspacePath, 'result'), - remoteUrl, - auth: { - username: 'x-access-token', - password: token, - }, - logger, - }); - - const catalogInfoUrl = remoteUrl.replace( - /\.git$/, - '/blob/master/catalog-info.yaml', - ); - - try { - await enableBranchProtectionOnDefaultRepoBranch({ - owner, - client, - repoName: name, - logger, - }); - } catch (e) { - throw new Error(`Failed to add branch protection to '${name}', ${e}`); - } - - return { remoteUrl, catalogInfoUrl }; - } - - private async createRemote(opts: { - client: Octokit; - access: string; - name: string; - owner: string; - description: string; - }) { - const { client, access, description, owner, name } = opts; - - const user = await client.users.getByUsername({ - username: owner, - }); - - const repoCreationPromise = - user.data.type === 'Organization' - ? client.repos.createInOrg({ - name, - org: owner, - private: this.config.repoVisibility !== 'public', - visibility: this.config.repoVisibility, - description, - }) - : client.repos.createForAuthenticatedUser({ - name, - private: this.config.repoVisibility === 'private', - description, - }); - - const { data: newRepo } = await repoCreationPromise; - - try { - if (access?.startsWith(`${owner}/`)) { - const [, team] = access.split('/'); - await client.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: team, - owner, - repo: name, - permission: 'admin', - }); - // no need to add access if it's the person who owns the personal account - } else if (access && access !== owner) { - await client.repos.addCollaborator({ - owner, - repo: name, - username: access, - permission: 'admin', - }); - } - } catch (e) { - throw new Error( - `Failed to add access to '${access}'. Status ${e.status} ${e.message}`, - ); - } - - return newRepo.clone_url; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts deleted file mode 100644 index 5c6a01afc8..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -jest.mock('@gitbeaker/node', () => ({ - Gitlab: jest.fn(), -})); - -jest.mock('./helpers'); - -import os from 'os'; -import path from 'path'; -import { GitlabPublisher } from './gitlab'; -import { Gitlab } from '@gitbeaker/node'; -import { initRepoAndPush } from './helpers'; -import { getVoidLogger } from '@backstage/backend-common'; - -describe('GitLab Publisher', () => { - const logger = getVoidLogger(); - const mockGitlabClient = { - Namespaces: { - show: jest.fn(), - }, - Projects: { - create: jest.fn(), - }, - Users: { - current: jest.fn(), - }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - - ((Gitlab as unknown) as jest.Mock).mockImplementation( - () => mockGitlabClient, - ); - }); - - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const resultPath = path.resolve(workspacePath, 'result'); - - describe('publish: createRemoteInGitLab', () => { - it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { - const publisher = await GitlabPublisher.fromConfig( - { - host: 'gitlab.com', - apiBaseUrl: 'https://gitlab.com/api/v4', - token: 'fake-token', - baseUrl: 'https://gitlab.hosted.com', - }, - { repoVisibility: 'public' }, - ); - - mockGitlabClient.Namespaces.show.mockResolvedValue({ - id: 42, - } as { id: number }); - mockGitlabClient.Projects.create.mockResolvedValue({ - http_url_to_repo: 'mockclone', - } as { http_url_to_repo: string }); - - const result = await publisher!.publish({ - values: { - isOrg: true, - storePath: 'https://gitlab.com/blam/test', - owner: 'bob', - }, - workspacePath, - logger, - }); - - expect(Gitlab).toHaveBeenCalledWith({ - token: 'fake-token', - host: 'https://gitlab.hosted.com', - }); - expect(result).toEqual({ - remoteUrl: 'mockclone', - catalogInfoUrl: 'mockclone', - }); - expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ - namespace_id: 42, - name: 'test', - visibility: 'public', - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'mockclone', - auth: { username: 'oauth2', password: 'fake-token' }, - logger, - }); - }); - - it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => { - const publisher = await GitlabPublisher.fromConfig( - { - host: 'gitlab.com', - apiBaseUrl: 'https://gitlab.com/api/v4', - token: 'fake-token', - baseUrl: 'https://gitlab.com', - }, - { repoVisibility: 'public' }, - ); - - mockGitlabClient.Namespaces.show.mockResolvedValue({}); - mockGitlabClient.Users.current.mockResolvedValue({ - id: 21, - } as { id: number }); - mockGitlabClient.Projects.create.mockResolvedValue({ - http_url_to_repo: 'mockclone', - } as { http_url_to_repo: string }); - - const result = await publisher!.publish({ - values: { - storePath: 'https://gitlab.com/blam/test', - owner: 'bob', - }, - workspacePath, - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'mockclone', - catalogInfoUrl: 'mockclone', - }); - expect(mockGitlabClient.Users.current).toHaveBeenCalled(); - expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ - namespace_id: 21, - name: 'test', - visibility: 'public', - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: resultPath, - remoteUrl: 'mockclone', - auth: { username: 'oauth2', password: 'fake-token' }, - logger, - }); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts deleted file mode 100644 index d2f58900a8..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { Gitlab } from '@gitbeaker/node'; -import { Gitlab as GitlabClient } from '@gitbeaker/core'; -import { initRepoAndPush } from './helpers'; -import parseGitUrl from 'git-url-parse'; -import path from 'path'; -import { GitLabIntegrationConfig } from '@backstage/integration'; - -export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; - -export class GitlabPublisher implements PublisherBase { - static async fromConfig( - config: GitLabIntegrationConfig, - { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, - ) { - if (!config.token) { - return undefined; - } - - const client = new Gitlab({ host: config.baseUrl, token: config.token }); - return new GitlabPublisher({ - token: config.token, - client, - repoVisibility, - }); - } - - constructor( - private readonly config: { - token: string; - client: GitlabClient; - repoVisibility: RepoVisibilityOptions; - }, - ) {} - - async publish({ - values, - workspacePath, - logger, - }: PublisherOptions): Promise { - const { owner, name } = parseGitUrl(values.storePath); - - const remoteUrl = await this.createRemote({ - owner, - name, - }); - - await initRepoAndPush({ - dir: path.join(workspacePath, 'result'), - remoteUrl, - auth: { - username: 'oauth2', - password: this.config.token, - }, - logger, - }); - - const catalogInfoUrl = remoteUrl.replace( - /\.git$/, - '/-/blob/master/catalog-info.yaml', - ); - return { remoteUrl, catalogInfoUrl }; - } - - private async createRemote(opts: { name: string; owner: string }) { - const { owner, name } = opts; - - // TODO(blam): this needs cleaning up to be nicer. The amount of brackets is too damn high! - // Shouldn't have to cast things now - let targetNamespace = ((await this.config.client.Namespaces.show( - owner, - )) as { - id: number; - }).id; - - if (!targetNamespace) { - targetNamespace = ((await this.config.client.Users.current()) as { - id: number; - }).id; - } - - const project = (await this.config.client.Projects.create({ - namespace_id: targetNamespace, - name: name, - visibility: this.config.repoVisibility, - })) as { http_url_to_repo: string }; - - return project?.http_url_to_repo; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts deleted file mode 100644 index 7a384f1657..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export { AzurePublisher } from './azure'; -export { BitbucketPublisher } from './bitbucket'; -export { GithubPublisher } from './github'; -export type { RepoVisibilityOptions } from './github'; -export { GitlabPublisher } from './gitlab'; -export { Publishers } from './publishers'; -export type { - PublisherBase, - PublisherBuilder, - PublisherOptions, - PublisherResult, -} from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts deleted file mode 100644 index c7ca832752..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Publishers } from './publishers'; -import { GithubPublisher } from './github'; -import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { AzurePublisher } from './azure'; -import { GitlabPublisher } from './gitlab'; -import { BitbucketPublisher } from './bitbucket'; - -jest.mock('@octokit/rest'); -jest.mock('azure-devops-node-api'); - -describe('Publishers', () => { - const logger = getVoidLogger(); - - it('should throw an error when the publisher for the source location is not registered', () => { - const publishers = new Publishers(); - - expect(() => publishers.get('https://github.com/org/repo')).toThrow( - expect.objectContaining({ - message: - 'Unable to find a publisher for URL: https://github.com/org/repo. Please make sure to register this host under an integration in app-config', - }), - ); - }); - - it('should return the correct preparer when the source matches for github', async () => { - const publishers = await Publishers.fromConfig( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'blob' }], - }, - }), - { - logger, - }, - ); - - expect(publishers.get('https://github.com/org/repo')).toBeInstanceOf( - GithubPublisher, - ); - }); - - it('should return the correct preparer when the source matches for azure', async () => { - const publishers = await Publishers.fromConfig( - new ConfigReader({ - integrations: { - azure: [{ host: 'dev.azure.com', token: 'blob' }], - }, - }), - { - logger, - }, - ); - - expect( - publishers.get('https://dev.azure.com/org/project/_git/repo'), - ).toBeInstanceOf(AzurePublisher); - }); - - it('should return the correct preparer when the source matches for bitbucket', async () => { - const publishers = await Publishers.fromConfig( - new ConfigReader({ - integrations: { - bitbucket: [ - { host: 'bitbucket.com', username: 'foo', token: 'blob' }, - ], - }, - }), - { - logger, - }, - ); - expect(publishers.get('https://bitbucket.org/owner/repo')).toBeInstanceOf( - BitbucketPublisher, - ); - }); - - it('should return the correct preparer when the source matches for gitlab', async () => { - const publishers = await Publishers.fromConfig( - new ConfigReader({ - integrations: { - gitlab: [{ host: 'gitlab.com', token: 'blob' }], - }, - }), - { - logger, - }, - ); - expect(publishers.get('https://gitlab.com/owner/repo')).toBeInstanceOf( - GitlabPublisher, - ); - }); - - it('should respect registrations for custom URLs for providers using the integrations config', async () => { - const publishers = await Publishers.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { host: 'my.special.github.enterprise.thing', token: 'lolghe' }, - ], - }, - }), - { - logger, - }, - ); - - expect( - publishers.get('https://my.special.github.enterprise.thing/org/repo'), - ).toBeInstanceOf(GithubPublisher); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts deleted file mode 100644 index 9c34ab5783..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; -import { PublisherBase, PublisherBuilder } from './types'; -import { - GithubPublisher, - RepoVisibilityOptions as GithubRepoVisibilityOptions, -} from './github'; -import { - GitlabPublisher, - RepoVisibilityOptions as GitlabRepoVisibilityOptions, -} from './gitlab'; -import { AzurePublisher } from './azure'; -import { - BitbucketPublisher, - RepoVisibilityOptions as BitbucketRepoVisibilityOptions, -} from './bitbucket'; -import { Logger } from 'winston'; -import { ScmIntegrations } from '@backstage/integration'; - -export class Publishers implements PublisherBuilder { - private publisherMap = new Map(); - - register(host: string, preparer: PublisherBase | undefined) { - this.publisherMap.set(host, preparer); - } - - get(url: string): PublisherBase { - const preparer = this.publisherMap.get(new URL(url).host); - if (!preparer) { - throw new Error( - `Unable to find a publisher for URL: ${url}. Please make sure to register this host under an integration in app-config`, - ); - } - return preparer; - } - - static async fromConfig( - config: Config, - _options: { logger: Logger }, - ): Promise { - const publishers = new Publishers(); - - const scm = ScmIntegrations.fromConfig(config); - - for (const integration of scm.azure.list()) { - const publisher = await AzurePublisher.fromConfig(integration.config); - if (publisher) { - publishers.register(integration.config.host, publisher); - } - } - - for (const integration of scm.github.list()) { - const repoVisibility = (config.getOptionalString( - 'scaffolder.github.visibility', - ) ?? 'public') as GithubRepoVisibilityOptions; - - const publisher = await GithubPublisher.fromConfig(integration.config, { - repoVisibility, - }); - if (publisher) { - publishers.register(integration.config.host, publisher); - } - } - - for (const integration of scm.gitlab.list()) { - const repoVisibility = (config.getOptionalString( - 'scaffolder.gitlab.visibility', - ) ?? 'public') as GitlabRepoVisibilityOptions; - - const publisher = await GitlabPublisher.fromConfig(integration.config, { - repoVisibility, - }); - - if (publisher) { - publishers.register(integration.config.host, publisher); - } - } - - for (const integration of scm.bitbucket.list()) { - const repoVisibility = (config.getOptionalString( - 'scaffolder.bitbucket.visibility', - ) ?? 'public') as BitbucketRepoVisibilityOptions; - - const publisher = await BitbucketPublisher.fromConfig( - integration.config, - { - repoVisibility, - }, - ); - - if (publisher) { - publishers.register(integration.config.host, publisher); - } - } - - return publishers; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts deleted file mode 100644 index 725816c918..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { TemplaterValues } from '../templater'; -import { Logger } from 'winston'; - -/** - * Publisher is in charge of taking a folder created by - * the templater, and pushing it to a remote storage - */ -export type PublisherBase = { - /** - * - * @param opts object containing the template entity from the service - * catalog, plus the values from the form and the directory that has - * been templated - */ - publish(opts: PublisherOptions): Promise; -}; - -export type PublisherOptions = { - values: TemplaterValues; - workspacePath: string; - logger: Logger; -}; - -export type PublisherResult = { - remoteUrl: string; - catalogInfoUrl?: string; -}; - -export type PublisherBuilder = { - register(host: string, publisher: PublisherBase): void; - get(storePath: string): PublisherBase; -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts deleted file mode 100644 index 1db5b52ffa..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const runCommand = jest.fn(); -const commandExists = jest.fn(); - -jest.mock('./helpers', () => ({ runCommand })); -jest.mock('command-exists', () => commandExists); -jest.mock('fs-extra'); - -import { ContainerRunner } from '@backstage/backend-common'; -import fs from 'fs-extra'; -import parseGitUrl from 'git-url-parse'; -import path from 'path'; -import { PassThrough } from 'stream'; -import { CookieCutter } from './cookiecutter'; - -describe('CookieCutter Templater', () => { - const containerRunner: jest.Mocked = { - runContainer: jest.fn(), - }; - - beforeEach(() => { - jest.clearAllMocks(); - commandExists.mockRejectedValue(null); - }); - - it('should write a cookiecutter.json file with the values from the entity', async () => { - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - description: 'description', - component_id: 'newthing', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }; - - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - - const templater = new CookieCutter({ containerRunner }); - await templater.run({ - workspacePath: 'tempdir', - values, - }); - - expect(fs.ensureDir).toBeCalledWith(path.join('tempdir', 'intermediate')); - expect(fs.writeJson).toBeCalledWith( - path.join('tempdir', 'template', 'cookiecutter.json'), - expect.objectContaining(values), - ); - }); - - it('should merge any value that is in the cookiecutter.json path already', async () => { - const existingJson = { - _copy_without_render: ['./github/workflows/*'], - }; - - jest - .spyOn(fs, 'readJSON') - .mockImplementationOnce(() => Promise.resolve(existingJson)); - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - component_id: 'something', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }; - - const templater = new CookieCutter({ containerRunner }); - await templater.run({ - workspacePath: 'tempdir', - values, - }); - - expect(fs.writeJSON).toBeCalledWith( - path.join('tempdir', 'template', 'cookiecutter.json'), - { - ...existingJson, - ...values, - destination: { - git: expect.objectContaining({ organization: 'org', name: 'repo' }), - }, - }, - ); - }); - - it('should throw an error if the cookiecutter json is malformed and not missing', async () => { - jest.spyOn(fs, 'readJSON').mockImplementationOnce(() => { - throw new Error('BAM'); - }); - - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }; - - const templater = new CookieCutter({ containerRunner }); - await expect( - templater.run({ - workspacePath: 'tempdir', - values, - }), - ).rejects.toThrow('BAM'); - }); - - it('should run the correct docker container with the correct bindings for the volumes', async () => { - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - component_id: 'newthing', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }; - - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - jest - .spyOn(fs, 'realpath') - .mockImplementation(x => Promise.resolve(x.toString())); - - const templater = new CookieCutter({ containerRunner }); - await templater.run({ - workspacePath: 'tempdir', - values, - }); - - expect(containerRunner.runContainer).toHaveBeenCalledWith({ - imageName: 'spotify/backstage-cookiecutter', - command: 'cookiecutter', - args: ['--no-input', '-o', '/output', '/input', '--verbose'], - envVars: { HOME: '/tmp' }, - mountDirs: { - [path.join('tempdir', 'template')]: '/input', - [path.join('tempdir', 'intermediate')]: '/output', - }, - workingDir: '/input', - logStream: undefined, - }); - }); - - it('should run the docker container mentioned in configs, overriding the default', async () => { - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - imageName: 'foo/cookiecutter-image-with-extensions', - }; - - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - - const templater = new CookieCutter({ containerRunner }); - await templater.run({ - workspacePath: 'tempdir', - values, - }); - - expect(containerRunner.runContainer).toHaveBeenCalledWith( - expect.objectContaining({ - imageName: 'foo/cookiecutter-image-with-extensions', - }), - ); - }); - - it('should pass through the streamer to the run docker helper', async () => { - const stream = new PassThrough(); - - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - component_id: 'newthing', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }; - - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - - const templater = new CookieCutter({ containerRunner }); - await templater.run({ - workspacePath: 'tempdir', - values, - logStream: stream, - }); - - expect(containerRunner.runContainer).toHaveBeenCalledWith({ - imageName: 'spotify/backstage-cookiecutter', - command: 'cookiecutter', - args: ['--no-input', '-o', '/output', '/input', '--verbose'], - envVars: { HOME: '/tmp' }, - mountDirs: { - [path.join('tempdir', 'template')]: '/input', - [path.join('tempdir', 'intermediate')]: '/output', - }, - workingDir: '/input', - logStream: stream, - }); - }); - - describe('when cookiecutter is available', () => { - it('use the binary', async () => { - const stream = new PassThrough(); - - const values = { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - component_id: 'newthing', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }; - - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - commandExists.mockResolvedValueOnce(true); - - const templater = new CookieCutter({ containerRunner }); - await templater.run({ - workspacePath: 'tempdir', - values, - logStream: stream, - }); - - expect(runCommand).toHaveBeenCalledWith({ - command: 'cookiecutter', - args: expect.arrayContaining([ - '--no-input', - '-o', - path.join('tempdir', 'intermediate'), - path.join('tempdir', 'template'), - '--verbose', - ]), - 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 CookieCutter({ containerRunner }); - await expect( - templater.run({ - workspacePath: 'tempdir', - values: { - owner: 'blobby', - storePath: 'https://github.com/org/repo', - destination: { - git: parseGitUrl('https://github.com/org/repo'), - }, - }, - logStream: stream, - }), - ).rejects.toThrow(/No data generated by cookiecutter/); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts deleted file mode 100644 index ef51fc20a9..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ContainerRunner } from '@backstage/backend-common'; -import { JsonValue } from '@backstage/config'; -import commandExists from 'command-exists'; -import fs from 'fs-extra'; -import path from 'path'; -import { runCommand } from './helpers'; -import { TemplaterBase, TemplaterRunOptions } from './types'; - -export class CookieCutter implements TemplaterBase { - private readonly containerRunner: ContainerRunner; - - constructor({ containerRunner }: { containerRunner: ContainerRunner }) { - this.containerRunner = containerRunner; - } - - private async fetchTemplateCookieCutter( - directory: string, - ): Promise> { - try { - return await fs.readJSON(path.join(directory, 'cookiecutter.json')); - } catch (ex) { - if (ex.code !== 'ENOENT') { - throw ex; - } - - return {}; - } - } - - public async run({ - workspacePath, - values, - logStream, - }: TemplaterRunOptions): Promise { - const templateDir = path.join(workspacePath, 'template'); - const intermediateDir = path.join(workspacePath, 'intermediate'); - await fs.ensureDir(intermediateDir); - const resultDir = path.join(workspacePath, 'result'); - - // First lets grab the default cookiecutter.json file - const cookieCutterJson = await this.fetchTemplateCookieCutter(templateDir); - - const { imageName, ...valuesForCookieCutterJson } = values; - const cookieInfo = { - ...cookieCutterJson, - ...valuesForCookieCutterJson, - }; - - await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); - - // Directories to bind on container - const mountDirs = { - [templateDir]: '/input', - [intermediateDir]: '/output', - }; - - // the command-exists package returns `true` or throws an error - const cookieCutterInstalled = await commandExists('cookiecutter').catch( - () => false, - ); - if (cookieCutterInstalled) { - await runCommand({ - command: 'cookiecutter', - args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], - logStream, - }); - } else { - await this.containerRunner.runContainer({ - imageName: imageName || 'spotify/backstage-cookiecutter', - command: 'cookiecutter', - args: ['--no-input', '-o', '/output', '/input', '--verbose'], - 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 cookiecutter was successful, intermediateDir will contain - // exactly one directory. - const [generated] = await fs.readdir(intermediateDir); - - if (generated === undefined) { - throw new Error('No data generated by cookiecutter'); - } - - await fs.move(path.join(intermediateDir, generated), resultDir); - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra/index.ts deleted file mode 100644 index 46bd9094ac..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra/index.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ContainerRunner } from '@backstage/backend-common'; -import fs from 'fs-extra'; -import path from 'path'; -import * as yaml from 'yaml'; -import { TemplaterBase, TemplaterRunOptions } from '../types'; - -// TODO(blam): Replace with the universal import from github-actions after a release -// As it will break the E2E without it -const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; - -export class CreateReactAppTemplater implements TemplaterBase { - private readonly containerRunner: ContainerRunner; - - constructor({ containerRunner }: { containerRunner: ContainerRunner }) { - this.containerRunner = containerRunner; - } - - public async run({ - workspacePath, - values, - logStream, - }: TemplaterRunOptions): Promise { - const { - component_id: componentName, - use_typescript: withTypescript, - use_github_actions: withGithubActions, - description, - owner, - } = values; - const intermediateDir = path.join(workspacePath, 'template'); - await fs.ensureDir(intermediateDir); - - const mountDirs = { - [intermediateDir]: '/template', - [intermediateDir]: '/result', - }; - - await this.containerRunner.runContainer({ - imageName: 'node:lts-alpine', - command: ['npx'], - args: [ - 'create-react-app', - componentName as string, - withTypescript ? ' --template typescript' : '', - ], - mountDirs, - workingDir: '/result', - logStream: logStream, - // 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' }, - }); - - // if cookiecutter was successful, intermediateDir will contain - // exactly one directory. - const [generated] = await fs.readdir(intermediateDir); - - if (generated === undefined) { - throw new Error('No data generated by cookiecutter'); - } - - const resultDir = path.join(workspacePath, 'result'); - await fs.move(path.join(intermediateDir, generated), resultDir); - - const extraAnnotations: Record = {}; - if (withGithubActions) { - await fs.mkdir(`${resultDir}/.github`); - await fs.mkdir(`${resultDir}/.github/workflows`); - const githubActionsYaml = ` -name: CRA Build - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [12.x] - - steps: - - name: checkout code - uses: actions/checkout@v1 - - name: get yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 - with: - path: \${{ steps.yarn-cache.outputs.dir }} - key: \${{ runner.os }}-yarn-\${{ hashFiles('**/yarn.lock') }} - restore-keys: | - \${{ runner.os }}-yarn- - - name: use node.js \${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: \${{ matrix.node-version }} - - name: yarn install, build, and test - working-directory: . - run: | - yarn install - yarn build - yarn test - env: - CI: true - `; - await fs.writeFile( - `${resultDir}/.github/workflows/main.yml`, - githubActionsYaml, - ); - - extraAnnotations[ - GITHUB_ACTIONS_ANNOTATION - ] = `${values?.destination?.git?.owner}/${values?.destination?.git?.name}`; - } - - const componentInfo = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: componentName, - description, - annotations: { - ...extraAnnotations, - }, - }, - spec: { - type: 'website', - lifecycle: 'experimental', - owner, - }, - }; - - await fs.writeFile( - `${resultDir}/catalog-info.yaml`, - yaml.stringify(componentInfo), - ); - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts deleted file mode 100644 index c2742428a5..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { InputError } from '@backstage/errors'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { spawn } from 'child_process'; -import { PassThrough, Writable } from 'stream'; - -export type RunCommandOptions = { - command: string; - args: string[]; - logStream?: Writable; -}; - -/** - * Gets the templater key to use for templating from the entity - * @param entity Template entity - */ -export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => { - const { templater } = entity.spec; - - if (!templater) { - throw new InputError('Template does not have a required templating key'); - } - - return templater; -}; - -/** - * - * @param options the options object - * @param options.command the command to run - * @param options.args the arguments to pass the command - * @param options.logStream the log streamer to capture log messages - */ -export const runCommand = async ({ - command, - args, - logStream = new PassThrough(), -}: RunCommandOptions) => { - await new Promise((resolve, reject) => { - const process = spawn(command, args); - - process.stdout.on('data', stream => { - logStream.write(stream); - }); - - process.stderr.on('data', stream => { - logStream.write(stream); - }); - - process.on('error', error => { - return reject(error); - }); - - process.on('close', code => { - if (code !== 0) { - return reject(`Command ${command} failed, exit code: ${code}`); - } - return resolve(); - }); - }); -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts deleted file mode 100644 index 4d80f86946..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from './cookiecutter'; -export * from './types'; -export * from './helpers'; -export * from './templaters'; -export * from './cra'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.test.ts deleted file mode 100644 index ae3890c092..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ContainerRunner } from '@backstage/backend-common'; -import { CookieCutter } from './cookiecutter'; -import { Templaters } from './templaters'; - -describe('Templaters', () => { - const containerRunner: jest.Mocked = { - runContainer: jest.fn(), - }; - - it('should throw an error when the templater is not registered', () => { - const templaters = new Templaters(); - - expect(() => templaters.get('cookiecutter')).toThrow( - expect.objectContaining({ - message: 'No templater registered for template: "cookiecutter"', - }), - ); - }); - it('should return the correct templater when the templater matches', () => { - const templaters = new Templaters(); - const templater = new CookieCutter({ containerRunner }); - - templaters.register('cookiecutter', templater); - - expect(templaters.get('cookiecutter')).toBe(templater); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.ts deleted file mode 100644 index a977502a01..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/templaters.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - TemplaterBase, - SupportedTemplatingKey, - TemplaterBuilder, -} from './types'; - -export class Templaters implements TemplaterBuilder { - private templaterMap = new Map(); - - register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase) { - this.templaterMap.set(templaterKey, templater); - } - - get(templaterId: string): TemplaterBase { - const templater = this.templaterMap.get(templaterId); - - if (!templater) { - throw new Error(`No templater registered for template: "${templaterId}"`); - } - - return templater; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts deleted file mode 100644 index 4d6b96c776..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import gitUrlParse from 'git-url-parse'; -import type { Writable } from 'stream'; - -/** - * Currently the required template values. The owner - * and where to store the result from templating - */ -export type RequiredTemplateValues = { - owner: string; - storePath: string; - destination?: { - git?: gitUrlParse.GitUrl; - }; -}; - -export type TemplaterValues = RequiredTemplateValues & Record; - -/** - * The returned directory from the templater which is ready - * to pass to the next stage of the scaffolder which is publishing - */ -export type TemplaterRunResult = { - resultDir: string; -}; - -/** - * The values that the templater will receive. The directory of the - * skeleton, with the values from the frontend. A dedicated log stream and a docker - * client to run any templater on top of your directory. - */ -export type TemplaterRunOptions = { - workspacePath: string; - values: TemplaterValues; - logStream?: Writable; -}; - -export type TemplaterBase = { - // runs the templating with the values and returns the directory to push the VCS - run(opts: TemplaterRunOptions): Promise; -}; - -export type TemplaterConfig = { - templater?: TemplaterBase; -}; - -/** - * List of supported templating options - */ -export type SupportedTemplatingKey = 'cookiecutter' | string; - -/** - * The templater builder holds the templaters ready for run time - */ -export type TemplaterBuilder = { - register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void; - get(templater: string): TemplaterBase; -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts deleted file mode 100644 index fef92d081f..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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. - */ - -import { resolve as resolvePath, dirname } from 'path'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import parseGitUrl from 'git-url-parse'; -import { TaskSpec } from './types'; -import { - getTemplaterKey, - joinGitUrlPath, - parseLocationAnnotation, - TemplaterValues, -} from '../stages'; - -export function templateEntityToSpec( - template: TemplateEntityV1alpha1, - inputValues: TemplaterValues, -): TaskSpec { - const steps: TaskSpec['steps'] = []; - - const { protocol, location } = parseLocationAnnotation(template); - - let url: string; - if (protocol === 'file') { - const path = resolvePath(dirname(location), template.spec.path || '.'); - - url = `file://${path}`; - } else { - url = joinGitUrlPath(location, template.spec.path); - } - const templater = getTemplaterKey(template); - - const values = { - ...inputValues, - destination: { - git: parseGitUrl(inputValues.storePath), - }, - } as TemplaterValues; - - steps.push({ - id: 'prepare', - name: 'Prepare', - action: 'legacy:prepare', - input: { - protocol, - url, - }, - }); - - steps.push({ - id: 'template', - name: 'Template', - action: 'legacy:template', - input: { - templater, - values, - }, - }); - - steps.push({ - id: 'publish', - name: 'Publish', - action: 'legacy:publish', - input: { - values, - }, - }); - - steps.push({ - id: 'register', - name: 'Register', - action: 'catalog:register', - input: { - catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}', - }, - }); - - return { - baseUrl: undefined, // not used by legacy actions - values: {}, - steps, - output: { - remoteUrl: '{{ steps.publish.output.remoteUrl }}', - catalogInfoUrl: '{{ steps.register.output.catalogInfoUrl }}', - entityRef: '{{ steps.register.output.entityRef }}', - }, - }; -} diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 17304aefb1..3ad4646b01 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -18,12 +18,6 @@ import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; -import { - PreparerBuilder, - TemplaterBuilder, - TemplaterValues, - PublisherBuilder, -} from '../scaffolder'; import { CatalogEntityClient } from '../lib/catalog'; import { validate } from 'jsonschema'; import { @@ -31,27 +25,21 @@ import { StorageTaskBroker, TaskWorker, } from '../scaffolder/tasks'; -import { templateEntityToSpec } from '../scaffolder/tasks/TemplateConverter'; import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry'; -import { createLegacyActions } from '../scaffolder/stages/legacy'; import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; -import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; +import { + ContainerRunner, + PluginDatabaseManager, + UrlReader, +} from '@backstage/backend-common'; import { InputError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; -import { - TemplateEntityV1alpha1, - TemplateEntityV1beta2, - Entity, -} from '@backstage/catalog-model'; +import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '../scaffolder/actions'; import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; export interface RouterOptions { - preparers: PreparerBuilder; - templaters: TemplaterBuilder; - publishers: PublisherBuilder; - logger: Logger; config: Config; reader: UrlReader; @@ -59,19 +47,11 @@ export interface RouterOptions { catalogClient: CatalogApi; actions?: TemplateAction[]; taskWorkers?: number; -} - -function isAlpha1Template( - entity: TemplateEntityV1alpha1 | TemplateEntityV1beta2, -): entity is TemplateEntityV1alpha1 { - return ( - entity.apiVersion === 'backstage.io/v1alpha1' || - entity.apiVersion === 'backstage.io/v1beta1' - ); + containerRunner: ContainerRunner; } function isBeta2Template( - entity: TemplateEntityV1alpha1 | TemplateEntityV1beta2, + entity: TemplateEntityV1beta2, ): entity is TemplateEntityV1beta2 { return entity.apiVersion === 'backstage.io/v1beta2'; } @@ -83,15 +63,13 @@ export async function createRouter( router.use(express.json()); const { - preparers, - templaters, - publishers, logger: parentLogger, config, reader, database, catalogClient, actions, + containerRunner, taskWorkers, } = options; @@ -118,20 +96,13 @@ export async function createRouter( const actionsToRegister = Array.isArray(actions) ? actions - : [ - ...createLegacyActions({ - preparers, - publishers, - templaters, - }), - ...createBuiltinActions({ - integrations, - catalogClient, - templaters, - reader, - config, - }), - ]; + : createBuiltinActions({ + integrations, + catalogClient, + containerRunner, + reader, + config, + }); actionsToRegister.forEach(action => actionRegistry.register(action)); workers.forEach(worker => worker.start()); @@ -165,42 +136,6 @@ export async function createRouter( schema, })), }); - } else if (isAlpha1Template(template)) { - res.json({ - title: template.metadata.title ?? template.metadata.name, - steps: [ - { - title: 'Fill in template parameters', - schema: template.spec.schema, - }, - { - title: 'Choose owner and repo', - schema: { - type: 'object', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: - 'A full URL to the repository that should be created. e.g https://github.com/backstage/new-repo', - }, - access: { - type: 'string', - title: 'Access', - description: - 'Who should have access, in org/team or user format', - }, - }, - }, - }, - ], - }); } else { throw new InputError( `Unsupported apiVersion field in schema entity, ${ @@ -222,26 +157,14 @@ export async function createRouter( }) .post('/v2/tasks', async (req, res) => { const templateName: string = req.body.templateName; - const values: TemplaterValues = req.body.values; + const values = req.body.values; const token = getBearerToken(req.headers.authorization); const template = await entityClient.findTemplate(templateName, { token, }); let taskSpec; - if (isAlpha1Template(template)) { - logger.warn( - `[DEPRECATION] - Template: ${template.metadata.name} has version ${template.apiVersion} which is going to be deprecated. Please refer to https://backstage.io/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2 for help on migrating`, - ); - - const result = validate(values, template.spec.schema); - if (!result.valid) { - res.status(400).json({ errors: result.errors }); - return; - } - - taskSpec = templateEntityToSpec(template, values); - } else if (isBeta2Template(template)) { + if (isBeta2Template(template)) { for (const parameters of [template.spec.parameters ?? []].flat()) { const result = validate(values, parameters); From a02ea1e4e121c1d2f26b534488cc44d0c87f7efa Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 19 Jun 2021 02:39:44 +0200 Subject: [PATCH 17/47] breaking: some more changes in the frontend. removing support for v1alpha1 Signed-off-by: blam --- .../sample-templates/remote-templates.yaml | 2 +- .../ScaffolderPage/ScaffolderPage.tsx | 32 ++++++-------- .../components/TemplatePage/TemplatePage.tsx | 43 ------------------- .../src/filter/EntityFilterGroupsProvider.tsx | 14 +++--- plugins/scaffolder/src/filter/context.ts | 4 +- 5 files changed, 23 insertions(+), 72 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml index b89b21d5e3..7846a0994e 100644 --- a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml @@ -6,4 +6,4 @@ metadata: spec: type: url targets: - # - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 1d16e50568..29758f4ac3 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,7 +14,17 @@ * limitations under the License. */ -import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { EntityMeta, TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { + Content, + ContentHeader, + Header, + ItemCardGrid, + Lifecycle, + Page, + Progress, + SupportButton, +} from '@backstage/core-components'; import { useStarredEntities } from '@backstage/plugin-catalog-react'; import { Button, Link, makeStyles, Typography } from '@material-ui/core'; import StarIcon from '@material-ui/icons/Star'; @@ -30,18 +40,6 @@ import { TemplateCard } from '../TemplateCard'; import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { - Content, - ContentHeader, - Header, - ItemCardGrid, - Lifecycle, - Page, - Progress, - SupportButton, - WarningPanel, -} from '@backstage/core-components'; - const useStyles = makeStyles(theme => ({ contentWrapper: { display: 'grid', @@ -90,7 +88,7 @@ export const ScaffolderPageContents = () => { ); const [search, setSearch] = useState(''); const [matchingEntities, setMatchingEntities] = useState( - [] as TemplateEntityV1alpha1[], + [] as TemplateEntityV1beta2[], ); const matchesQuery = (metadata: EntityMeta, query: string) => @@ -175,11 +173,7 @@ export const ScaffolderPageContents = () => { {matchingEntities && matchingEntities?.length > 0 && matchingEntities.map((template, i) => ( - + ))} diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 3caece4878..b6cab5b620 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -16,7 +16,6 @@ import { JsonObject, JsonValue } from '@backstage/config'; import { LinearProgress } from '@material-ui/core'; import { FieldValidation, FormValidation, IChangeEvent } from '@rjsf/core'; -import parseGitUrl from 'git-url-parse'; import React, { useCallback, useState } from 'react'; import { generatePath, Navigate, useNavigate } from 'react-router'; import { useParams } from 'react-router-dom'; @@ -99,39 +98,6 @@ export const createValidator = ( }; }; -const storePathValidator = ( - formData: { storePath?: string }, - errors: FormValidation, -) => { - const { storePath } = formData; - if (!storePath) { - errors.storePath.addError('Store path is required and not present'); - return errors; - } - - try { - const parsedUrl = parseGitUrl(storePath); - - if (!parsedUrl.resource || !parsedUrl.owner || !parsedUrl.name) { - if (parsedUrl.resource === 'dev.azure.com') { - errors.storePath.addError( - "The store path should be formatted like https://dev.azure.com/{org}/{project}/_git/{repo} for Azure URL's", - ); - } else { - errors.storePath.addError( - 'The store path should be a complete Git URL to the new repository location. For example: https://github.com/{owner}/{repo}', - ); - } - } - } catch (ex) { - errors.storePath.addError( - `Failed validation of the store path with message ${ex.message}`, - ); - } - - return errors; -}; - export const TemplatePage = ({ customFieldExtensions = [], }: { @@ -203,15 +169,6 @@ export const TemplatePage = ({ onReset={handleFormReset} onFinish={handleCreate} steps={schema.steps.map(step => { - // TODO: Can delete this function when the migration from v1 to v2 beta is completed - // And just have the default validator for all fields. - if ((step.schema as any)?.properties?.storePath) { - return { - ...step, - validate: (a, b) => storePathValidator(a, b), - }; - } - return { ...step, validate: createValidator(step.schema, customFieldValidators), diff --git a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx index 7aa1a21643..a5be11380f 100644 --- a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useAsyncFn } from 'react-use'; @@ -50,7 +50,7 @@ function useProvideEntityFilters(): FilterGroupsContext { const response = await catalogApi.getEntities({ filter: { kind: 'Template' }, }); - return response.items as TemplateEntityV1alpha1[]; + return response.items as TemplateEntityV1beta2[]; }); const filterGroups = useRef<{ @@ -64,7 +64,7 @@ function useProvideEntityFilters(): FilterGroupsContext { [filterGroupId: string]: FilterGroupStates; }>({}); const [filteredEntities, setFilteredEntities] = useState< - TemplateEntityV1alpha1[] + TemplateEntityV1beta2[] >([]); const [availableCategories, setAvailableCategories] = useState([]); const [isCatalogEmpty, setCatalogEmpty] = useState(false); @@ -161,7 +161,7 @@ function buildStates( filterGroups: { [filterGroupId: string]: FilterGroup }, selectedFilterKeys: { [filterGroupId: string]: Set }, selectedCategories: string[], - entities?: TemplateEntityV1alpha1[], + entities?: TemplateEntityV1beta2[], error?: Error, ): { [filterGroupId: string]: FilterGroupStates } { // On error - all entries are an error state @@ -208,7 +208,7 @@ function buildStates( } // Given all entites, find all possible categories and provide them in a sorted list. -function collectCategories(entities?: TemplateEntityV1alpha1[]): string[] { +function collectCategories(entities?: TemplateEntityV1beta2[]): string[] { const categories = new Set(); (entities || []).forEach(e => { if (e.spec?.type) { @@ -224,9 +224,9 @@ function buildMatchingEntities( filterGroups: { [filterGroupId: string]: FilterGroup }, selectedFilterKeys: { [filterGroupId: string]: Set }, selectedCategories: string[], - entities?: TemplateEntityV1alpha1[], + entities?: TemplateEntityV1beta2[], excludeFilterGroupId?: string, -): TemplateEntityV1alpha1[] { +): TemplateEntityV1beta2[] { // Build one filter fn per filter group const allFilters: EntityFilterFn[] = []; for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) { diff --git a/plugins/scaffolder/src/filter/context.ts b/plugins/scaffolder/src/filter/context.ts index ee66e3d9da..385d3f6eb5 100644 --- a/plugins/scaffolder/src/filter/context.ts +++ b/plugins/scaffolder/src/filter/context.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { createContext } from 'react'; import { FilterGroup, FilterGroupStates } from './types'; @@ -32,7 +32,7 @@ export type FilterGroupsContext = { loading: boolean; error?: Error; filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; - filteredEntities: TemplateEntityV1alpha1[]; + filteredEntities: TemplateEntityV1beta2[]; availableCategories: string[]; isCatalogEmpty: boolean; }; From 18660e933765f2b93c328bc15f04af7c637cd050 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 30 Jun 2021 20:53:50 +0200 Subject: [PATCH 18/47] chore: fixing merge conflict Signed-off-by: blam --- .../src/scaffolder/actions/builtin/helpers.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index a024be4f67..e2459a8ca7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -1,9 +1,5 @@ /* -<<<<<<< HEAD:plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts - * Copyright 2020 The Backstage Authors -======= * Copyright 2021 The Backstage Authors ->>>>>>> breaking: removing alphav1 support for templates:plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From a41b024a0f092276104b92fdfcc84faf68aa8d87 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 30 Jun 2021 21:18:39 +0200 Subject: [PATCH 19/47] feat: removing the last parts of v1alpha1 and fixing up TypeScript Signed-off-by: blam --- .../builtin/fetch/cookiecutter.test.ts | 186 ------------------ .../actions/builtin/publish/azure.test.ts | 7 +- .../actions/builtin/publish/bitbucket.test.ts | 5 +- .../actions/builtin/publish/github.test.ts | 7 +- .../actions/builtin/publish/gitlab.test.ts | 6 +- .../src/service/router.test.ts | 36 +--- .../ScaffolderPage/ScaffolderPage.tsx | 1 + .../components/TemplateCard/TemplateCard.tsx | 6 +- 8 files changed, 25 insertions(+), 229 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts deleted file mode 100644 index 3ce3052956..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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. - */ -jest.mock('./helpers'); - -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { ScmIntegrations } from '@backstage/integration'; -import mock from 'mock-fs'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; -import { PassThrough } from 'stream'; -import { Templaters } from '../../../stages/templater'; -import { createFetchCookiecutterAction } from './cookiecutter'; -import { fetchContents } from './helpers'; - -describe('fetch:cookiecutter', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); - - const templaters = new Templaters(); - const cookiecutterTemplater = { run: jest.fn() }; - const mockTmpDir = os.tmpdir(); - const mockContext = { - input: { - url: 'https://google.com/cookie/cutter', - targetPath: 'something', - values: { - help: 'me', - }, - }, - baseUrl: 'somebase', - workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; - - const mockReader: UrlReader = { - read: jest.fn(), - readTree: jest.fn(), - search: jest.fn(), - }; - - const action = createFetchCookiecutterAction({ - integrations, - templaters, - reader: mockReader, - }); - - templaters.register('cookiecutter', cookiecutterTemplater); - - beforeEach(() => { - mock({ [`${mockContext.workspacePath}/result`]: {} }); - jest.restoreAllMocks(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should call fetchContents with the correct values', async () => { - await action.handler(mockContext); - - expect(fetchContents).toHaveBeenCalledWith({ - reader: mockReader, - integrations, - baseUrl: mockContext.baseUrl, - fetchUrl: mockContext.input.url, - outputPath: resolvePath( - mockContext.workspacePath, - `template/{{cookiecutter and 'contents'}}`, - ), - }); - }); - - it('should execute the cookiecutter templater with the correct values', async () => { - await action.handler(mockContext); - - expect(cookiecutterTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, - logStream: mockContext.logStream, - values: mockContext.input.values, - }); - }); - - it('should execute the cookiecutter templater with optional inputs if they are present and valid', async () => { - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - copyWithoutRender: ['goreleaser.yml'], - extensions: [ - 'jinja2_custom_filters_extension.string_filters_extension.StringFilterExtension', - ], - imageName: 'foo/cookiecutter-image-with-extensions', - }, - }); - - expect(cookiecutterTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, - logStream: mockContext.logStream, - values: { - ...mockContext.input.values, - _copy_without_render: ['goreleaser.yml'], - _extensions: [ - 'jinja2_custom_filters_extension.string_filters_extension.StringFilterExtension', - ], - imageName: 'foo/cookiecutter-image-with-extensions', - }, - }); - }); - - it('should throw if copyWithoutRender is not an Array', async () => { - await expect( - action.handler({ - ...mockContext, - input: { - ...mockContext.input, - copyWithoutRender: 'xyz', - }, - }), - ).rejects.toThrow(/copyWithoutRender must be an Array/); - }); - - it('should throw if extensions is not an Array', async () => { - await expect( - action.handler({ - ...mockContext, - input: { - ...mockContext.input, - extensions: 'xyz', - }, - }), - ).rejects.toThrow(/extensions must be an Array/); - }); - - it('should throw if there is no cookiecutter templater initialized', async () => { - const templatersWithoutCookiecutter = new Templaters(); - - const newAction = createFetchCookiecutterAction({ - integrations, - templaters: templatersWithoutCookiecutter, - reader: mockReader, - }); - - await expect(newAction.handler(mockContext)).rejects.toThrow( - /No templater registered/, - ); - }); - - 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( - /Relative path is not allowed to refer to a directory outside its parent/, - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 1dc29ea763..30995dee38 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -jest.mock('../../../stages/publish/helpers'); jest.mock('azure-devops-node-api', () => ({ WebApi: jest.fn(), getPersonalAccessTokenHandler: jest.fn().mockReturnValue(() => {}), })); +jest.mock('../helpers'); + import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { WebApi } from 'azure-devops-node-api'; import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; describe('publish:azure', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 0e5f8df72b..9fe245935b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -jest.mock('../../../stages/publish/helpers'); + +jest.mock('../helpers'); import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; @@ -23,7 +24,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; describe('publish:bitbucket', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 0b277c4739..f0725d6e8e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -jest.mock('../../../stages/publish/helpers'); + +jest.mock('../helpers'); jest.mock('@octokit/rest'); import { createPublishGithubAction } from './github'; @@ -21,7 +22,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; import { when } from 'jest-when'; describe('publish:github', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 25a129d905..5150205a08 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -jest.mock('../../../stages/publish/helpers'); +jest.mock('../helpers'); jest.mock('@gitbeaker/node'); import { createPublishGitlabAction } from './gitlab'; @@ -21,7 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { initRepoAndPush } from '../helpers'; describe('publish:gitlab', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index b43b62a413..d91ec2b7d0 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -33,12 +33,13 @@ import { PluginDatabaseManager, DatabaseManager, UrlReaders, + DockerContainerRunner, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import { Preparers, Publishers, Templaters } from '../scaffolder'; +import { TemplateEntityV1beta2 } from '../../../../packages/catalog-model/src'; import { createRouter } from './router'; const createCatalogClient = (templates: any[] = []) => @@ -66,8 +67,8 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; - const template = { - apiVersion: 'backstage.io/v1alpha1', + const template: TemplateEntityV1beta2 = { + apiVersion: 'backstage.io/v1beta2', kind: 'Template', metadata: { description: 'Create a new CRA website project', @@ -80,42 +81,19 @@ describe('createRouter', () => { }, spec: { owner: 'web@example.com', - path: '.', - schema: { - properties: { - component_id: { - description: 'Unique name of the component', - title: 'Name', - type: 'string', - }, - description: { - description: 'Description of the component', - title: 'Description', - type: 'string', - }, - use_typescript: { - default: true, - description: 'Include TypeScript', - title: 'Use TypeScript', - type: 'boolean', - }, - }, - required: ['component_id', 'use_typescript'], - }, - templater: 'cra', type: 'website', + steps: [], + parameters: [], }, }; beforeAll(async () => { const router = await createRouter({ logger: getVoidLogger(), - preparers: new Preparers(), - templaters: new Templaters(), - publishers: new Publishers(), config: new ConfigReader({}), database: createDatabase(), catalogClient: createCatalogClient([template]), + containerRunner: new DockerContainerRunner({} as any), reader: mockUrlReader, }); app = express().use(router); diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 29758f4ac3..65386b60b3 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -21,6 +21,7 @@ import { Header, ItemCardGrid, Lifecycle, + WarningPanel, Page, Progress, SupportButton, diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index ad91fc6cfd..9d2e827be9 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -16,7 +16,7 @@ import { Entity, RELATION_OWNED_BY, - TemplateEntityV1alpha1, + TemplateEntityV1beta2, } from '@backstage/catalog-model'; import { ScmIntegrationIcon, @@ -93,7 +93,7 @@ const useDeprecationStyles = makeStyles(theme => ({ })); export type TemplateCardProps = { - template: TemplateEntityV1alpha1; + template: TemplateEntityV1beta2; deprecated?: boolean; }; @@ -106,7 +106,7 @@ type TemplateProps = { }; const getTemplateCardProps = ( - template: TemplateEntityV1alpha1, + template: TemplateEntityV1beta2, ): TemplateProps & { key: string } => { return { key: template.metadata.uid!, From 7632873678f0360cabd0744614a5a37ec50f4ee8 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 11:02:31 +0200 Subject: [PATCH 20/47] chore: updating api-reports Signed-off-by: blam --- packages/catalog-model/api-report.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 68644bef87..5d055566be 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -492,29 +492,6 @@ export { SystemEntityV1alpha1 } // @public (undocumented) export const systemEntityV1alpha1Validator: KindValidator; -// @public (undocumented) -interface TemplateEntityV1alpha1 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; - // (undocumented) - kind: 'Template'; - // (undocumented) - spec: { - type: string; - templater: string; - path?: string; - schema: JSONSchema; - owner?: string; - }; -} - -export { TemplateEntityV1alpha1 as TemplateEntity } - -export { TemplateEntityV1alpha1 } - -// @public (undocumented) -export const templateEntityV1alpha1Validator: KindValidator; - // @public (undocumented) export interface TemplateEntityV1beta2 extends Entity { // (undocumented) From 70bb2a711f04b0bdc213add414556cd6776b6aff Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 14:26:07 +0200 Subject: [PATCH 21/47] chore: fix up some code review comments Signed-off-by: blam --- plugins/catalog-backend/src/ingestion/LocationReaders.ts | 1 - .../src/next/processing/DefaultCatalogProcessingOrchestrator.ts | 1 - plugins/scaffolder-backend/src/service/router.test.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 86a8ebb9f2..624c7aaa61 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -265,7 +265,6 @@ export class LocationReaders implements LocationReader { )}, ${e}`; emit(result.inputError(item.location, message)); logger.warn(message); - logger.debug(e.stack); return undefined; } } diff --git a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts index 235673db03..bb23b6afb3 100644 --- a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts @@ -124,7 +124,6 @@ export class DefaultCatalogProcessingOrchestrator }; } catch (error) { this.options.logger.warn(error.message); - this.options.logger.debug(error.stack); return { ok: false, errors: collector.results().errors.concat(error), diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index d91ec2b7d0..c693573c4f 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -39,7 +39,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import { TemplateEntityV1beta2 } from '../../../../packages/catalog-model/src'; +import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { createRouter } from './router'; const createCatalogClient = (templates: any[] = []) => From f0928d674fdd0f0336c13f0d6420a43cb7050d88 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 14:50:39 +0200 Subject: [PATCH 22/47] chore: fix e2e tests as they no longer need all of the Templaters and Publishers Signed-off-by: blam --- .../packages/backend/src/plugins/scaffolder.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 333ffa11df..ee40b06320 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -24,23 +24,11 @@ export default async function createPlugin({ const dockerClient = new Docker(); const containerRunner = new DockerContainerRunner({ dockerClient }); - const cookiecutterTemplater = new CookieCutter({ containerRunner }); - const craTemplater = new CreateReactAppTemplater({ containerRunner }); - const templaters = new Templaters(); - - templaters.register('cookiecutter', cookiecutterTemplater); - templaters.register('cra', craTemplater); - - const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); - const discovery = SingleHostDiscovery.fromConfig(config); const catalogClient = new CatalogClient({ discoveryApi: discovery }); return await createRouter({ - preparers, - templaters, - publishers, + containerRunner, logger, config, database, From 350555727fcb85c6ace5728b0977e590d6fb2f4c Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 14:56:18 +0200 Subject: [PATCH 23/47] chore: dont need to export this dep any more Signed-off-by: blam --- plugins/scaffolder-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 0a0ccd36b1..7173f865c5 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -39,7 +39,6 @@ "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", - "@types/git-url-parse": "^9.0.0", "azure-devops-node-api": "^10.2.2", "command-exists": "^1.2.9", "compression": "^1.7.4", @@ -68,6 +67,7 @@ "@backstage/test-utils": "^0.1.14", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", + "@types/git-url-parse": "^9.0.0", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", "jest-when": "^3.1.0", From b9cba53a05c85d442e6a6961b7ca1a857e8577d3 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 15:08:36 +0200 Subject: [PATCH 24/47] chore: removing the extra deps Signed-off-by: blam --- .../default-app/packages/backend/src/plugins/scaffolder.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index ee40b06320..a0201cec01 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -4,12 +4,7 @@ import { } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { - CookieCutter, - CreateReactAppTemplater, createRouter, - Preparers, - Publishers, - Templaters, } from '@backstage/plugin-scaffolder-backend'; import Docker from 'dockerode'; import { Router } from 'express'; From 4c689b0fae738935be3b5659e7524f244bc68dd7 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 16:18:22 +0200 Subject: [PATCH 25/47] chore: making the failing test pass because of parameter validation Signed-off-by: blam --- .../scaffolder-backend/src/service/router.test.ts | 13 +++++++++++-- plugins/scaffolder-backend/src/service/router.ts | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index c693573c4f..227f6d3c88 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -83,7 +83,16 @@ describe('createRouter', () => { owner: 'web@example.com', type: 'website', steps: [], - parameters: [], + parameters: { + type: 'object', + required: ['required'], + properties: { + required: { + type: 'string', + description: 'Required parameter', + }, + }, + }, }, }; @@ -117,7 +126,7 @@ describe('createRouter', () => { const response = await request(app) .post('/v2/tasks') .send({ - templateName: '', + templateName: 'create-react-app-template', values: { storePath: 'https://github.com/backstage/backstage', }, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 3ad4646b01..2b5bdb3bf8 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -164,6 +164,7 @@ export async function createRouter( }); let taskSpec; + if (isBeta2Template(template)) { for (const parameters of [template.spec.parameters ?? []].flat()) { const result = validate(values, parameters); From 2529ec28a6bad75a9c8e617873af491edea9a2ec Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 2 Jul 2021 16:21:49 +0200 Subject: [PATCH 26/47] chore: fixing all tests Signed-off-by: blam --- plugins/scaffolder-backend/src/service/router.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 227f6d3c88..c0fdcb2ef7 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -141,10 +141,7 @@ describe('createRouter', () => { .send({ templateName: 'create-react-app-template', values: { - storePath: 'https://github.com/backstage/backstage', - component_id: '123', - name: 'test', - use_typescript: false, + required: 'required-value', }, }); @@ -161,10 +158,7 @@ describe('createRouter', () => { .send({ templateName: 'create-react-app-template', values: { - storePath: 'https://github.com/backstage/backstage', - component_id: '123', - name: 'test', - use_typescript: false, + required: 'required-value', }, }); From b658d339442f30ac3ee209c8118fcdcb3139183f Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Jul 2021 20:38:27 +0200 Subject: [PATCH 27/47] chore: added some tests for cookiecutter but they need re-writing Signed-off-by: blam --- .../builtin/fetch/cookiecutter.test.ts | 194 ++++++++++++++++++ .../actions/builtin/fetch/cookiecutter.ts | 1 + 2 files changed, 195 insertions(+) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts new file mode 100644 index 0000000000..53f255a639 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts @@ -0,0 +1,194 @@ +/* + * 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. + */ +const runCommand = jest.fn(); +const commandExists = jest.fn(); +const fetchContents = jest.fn(); +jest.mock('./helpers', () => ({ runCommand, fetchContents })); +jest.mock('command-exists', () => commandExists); +jest.mock('./helpers'); +jest.mock('fs-extra'); + +import fs from 'fs-extra'; +import { + getVoidLogger, + UrlReader, + ContainerRunner, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import mock from 'mock-fs'; +import os from 'os'; +import { resolve as resolvePath } from 'path'; +import { PassThrough } from 'stream'; +import { createFetchCookiecutterAction } from './cookiecutter'; + +describe('fetch:cookiecutter', () => { + 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://google.com/cookie/cutter', + targetPath: 'something', + values: { + help: 'me', + }, + }, + baseUrl: 'somebase', + workspacePath: mockTmpDir, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + + const containerRunner: jest.Mocked = { + runContainer: jest.fn(), + }; + + const mockReader: UrlReader = { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const action = createFetchCookiecutterAction({ + integrations, + containerRunner, + reader: mockReader, + }); + + beforeEach(() => { + (fs.readdir as jest.Mock).mockReturnValueOnce(['cookiecutter.json']); + + mock({ + [`${mockContext.workspacePath}/template`]: { 'cookiecutter.json': '{}' }, + }); + mock({ [`${mockContext.workspacePath}/result`]: {} }); + mock({ + [`${mockContext.workspacePath}/intermediate`]: {}, + }); + jest.restoreAllMocks(); + commandExists.mockRejectedValue(null); + }); + + afterEach(() => { + mock.restore(); + }); + + it('should call fetchContents with the correct values', async () => { + await action.handler(mockContext); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: resolvePath( + mockContext.workspacePath, + `template/{{cookiecutter and 'contents'}}`, + ), + }); + }); + + it('should execute the cookiecutter templater with optional inputs if they are present and valid', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + copyWithoutRender: ['goreleaser.yml'], + extensions: [ + 'jinja2_custom_filters_extension.string_filters_extension.StringFilterExtension', + ], + imageName: 'foo/cookiecutter-image-with-extensions', + }, + }); + + expect(cookiecutterTemplater.run).toHaveBeenCalledWith({ + workspacePath: mockTmpDir, + logStream: mockContext.logStream, + values: { + ...mockContext.input.values, + _copy_without_render: ['goreleaser.yml'], + _extensions: [ + 'jinja2_custom_filters_extension.string_filters_extension.StringFilterExtension', + ], + imageName: 'foo/cookiecutter-image-with-extensions', + }, + }); + }); + + // it('should throw if copyWithoutRender is not an Array', async () => { + // await expect( + // action.handler({ + // ...mockContext, + // input: { + // ...mockContext.input, + // copyWithoutRender: 'xyz', + // }, + // }), + // ).rejects.toThrow(/copyWithoutRender must be an Array/); + // }); + + // it('should throw if extensions is not an Array', async () => { + // await expect( + // action.handler({ + // ...mockContext, + // input: { + // ...mockContext.input, + // extensions: 'xyz', + // }, + // }), + // ).rejects.toThrow(/extensions must be an Array/); + // }); + + // it('should throw if there is no cookiecutter templater initialized', async () => { + // const templatersWithoutCookiecutter = new Templaters(); + + // const newAction = createFetchCookiecutterAction({ + // integrations, + // templaters: templatersWithoutCookiecutter, + // reader: mockReader, + // }); + + // await expect(newAction.handler(mockContext)).rejects.toThrow( + // /No templater registered/, + // ); + // }); + + // 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( + // /Relative path is not allowed to refer to a directory outside its parent/, + // ); + // }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts index e5720204c7..2bf22ce4fd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts @@ -108,6 +108,7 @@ export class CookiecutterRunner { // if cookiecutter was successful, intermediateDir will contain // exactly one directory. + const [generated] = await fs.readdir(intermediateDir); if (generated === undefined) { From 2ed57f31c76a22358da80d5b5e9959d26d8e2036 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 6 Jul 2021 20:20:49 +0200 Subject: [PATCH 28/47] chore: write some tests for the cookiecutter templater Signed-off-by: blam --- .../builtin/fetch/cookiecutter.test.ts | 252 ++++++++++-------- 1 file changed, 138 insertions(+), 114 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts index 53f255a639..8bd205be1d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts @@ -16,24 +16,26 @@ const runCommand = jest.fn(); const commandExists = jest.fn(); const fetchContents = jest.fn(); -jest.mock('./helpers', () => ({ runCommand, fetchContents })); -jest.mock('command-exists', () => commandExists); -jest.mock('./helpers'); -jest.mock('fs-extra'); -import fs from 'fs-extra'; +jest.mock('./helpers', () => ({ fetchContents })); +jest.mock('command-exists', () => commandExists); +jest.mock('../helpers', () => ({ runCommand })); + import { getVoidLogger, UrlReader, ContainerRunner, } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; +import { ConfigReader, JsonObject } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import mock from 'mock-fs'; import os from 'os'; -import { resolve as resolvePath } from 'path'; import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; +import { join } from 'path'; +import { ActionContext } from '../../types'; + +import fs from 'fs-extra'; describe('fetch:cookiecutter', () => { const integrations = ScmIntegrations.fromConfig( @@ -48,21 +50,15 @@ describe('fetch:cookiecutter', () => { ); const mockTmpDir = os.tmpdir(); - const mockContext = { - input: { - url: 'https://google.com/cookie/cutter', - targetPath: 'something', - values: { - help: 'me', - }, - }, - baseUrl: 'somebase', - workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + + let mockContext: ActionContext<{ + url: string; + targetPath?: string; + values: JsonObject; + copyWithoutRender?: string[]; + extensions?: string[]; + imageName?: string; + }>; const containerRunner: jest.Mocked = { runContainer: jest.fn(), @@ -81,114 +77,142 @@ describe('fetch:cookiecutter', () => { }); beforeEach(() => { - (fs.readdir as jest.Mock).mockReturnValueOnce(['cookiecutter.json']); + jest.resetAllMocks(); - mock({ - [`${mockContext.workspacePath}/template`]: { 'cookiecutter.json': '{}' }, + mockContext = { + input: { + url: 'https://google.com/cookie/cutter', + targetPath: 'something', + values: { + help: 'me', + }, + }, + baseUrl: 'somebase', + workspacePath: mockTmpDir, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + + // mock the temp directory + mock({ [mockTmpDir]: {} }); + mock({ [`${join(mockTmpDir, 'template')}`]: {} }); + + commandExists.mockResolvedValue(null); + + // Mock when run container is called it creates some new files in the mock filesystem + containerRunner.runContainer.mockImplementation(async () => { + mock({ + [`${join(mockTmpDir, 'intermediate')}`]: { + 'testfile.json': '{}', + }, + }); }); - mock({ [`${mockContext.workspacePath}/result`]: {} }); - mock({ - [`${mockContext.workspacePath}/intermediate`]: {}, + + // Mock when runCommand is called it creats some new files in the mock filesystem + runCommand.mockImplementation(async () => { + mock({ + [`${join(mockTmpDir, 'intermediate')}`]: { + 'testfile.json': '{}', + }, + }); }); - jest.restoreAllMocks(); - commandExists.mockRejectedValue(null); }); afterEach(() => { mock.restore(); }); - it('should call fetchContents with the correct values', async () => { + it('should throw an error when copyWithoutRender is not an array', async () => { + (mockContext.input as any).copyWithoutRender = 'not an array'; + + await expect( + action.handler(mockContext), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fetch action input copyWithoutRender must be an Array"`, + ); + }); + + it('should throw an error when extensions is not an array', async () => { + (mockContext.input as any).extensions = 'not an array'; + + await expect( + action.handler(mockContext), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fetch action input extensions must be an Array"`, + ); + }); + + it('should call fetchContents with the correct variables', async () => { + fetchContents.mockImplementation(() => Promise.resolve()); + await action.handler(mockContext); + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + }); + + it('should call out to cookiecutter using runCommand when cookiecutter is installed', async () => { + commandExists.mockResolvedValue(true); + await action.handler(mockContext); - expect(fetchContents).toHaveBeenCalledWith({ - reader: mockReader, - integrations, - baseUrl: mockContext.baseUrl, - fetchUrl: mockContext.input.url, - outputPath: resolvePath( - mockContext.workspacePath, - `template/{{cookiecutter and 'contents'}}`, - ), - }); + expect(runCommand).toHaveBeenCalledWith( + expect.objectContaining({ + command: 'cookiecutter', + args: [ + '--no-input', + '-o', + join(mockTmpDir, 'intermediate'), + join(mockTmpDir, 'template'), + '--verbose', + ], + logStream: mockContext.logStream, + }), + ); }); - it('should execute the cookiecutter templater with optional inputs if they are present and valid', async () => { - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - copyWithoutRender: ['goreleaser.yml'], - extensions: [ - 'jinja2_custom_filters_extension.string_filters_extension.StringFilterExtension', - ], - imageName: 'foo/cookiecutter-image-with-extensions', - }, - }); + it('should call out to the containerRunner when there is no cookiecutter installed', async () => { + commandExists.mockResolvedValue(false); - expect(cookiecutterTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, - logStream: mockContext.logStream, - values: { - ...mockContext.input.values, - _copy_without_render: ['goreleaser.yml'], - _extensions: [ - 'jinja2_custom_filters_extension.string_filters_extension.StringFilterExtension', - ], - imageName: 'foo/cookiecutter-image-with-extensions', - }, - }); + await action.handler(mockContext); + + expect(containerRunner.runContainer).toHaveBeenCalledWith( + expect.objectContaining({ + imageName: 'spotify/backstage-cookiecutter', + command: 'cookiecutter', + args: ['--no-input', '-o', '/output', '/input', '--verbose'], + mountDirs: { + [join(mockTmpDir, 'intermediate')]: '/output', + [join(mockTmpDir, 'template')]: '/input', + }, + workingDir: '/input', + envVars: { HOME: '/tmp' }, + logStream: mockContext.logStream, + }), + ); }); - // it('should throw if copyWithoutRender is not an Array', async () => { - // await expect( - // action.handler({ - // ...mockContext, - // input: { - // ...mockContext.input, - // copyWithoutRender: 'xyz', - // }, - // }), - // ).rejects.toThrow(/copyWithoutRender must be an Array/); - // }); + it('should use a custom imageName when there is an image supplied to the context', async () => { + const imageName = 'test-image'; + mockContext.input.imageName = imageName; - // it('should throw if extensions is not an Array', async () => { - // await expect( - // action.handler({ - // ...mockContext, - // input: { - // ...mockContext.input, - // extensions: 'xyz', - // }, - // }), - // ).rejects.toThrow(/extensions must be an Array/); - // }); + await action.handler(mockContext); - // it('should throw if there is no cookiecutter templater initialized', async () => { - // const templatersWithoutCookiecutter = new Templaters(); - - // const newAction = createFetchCookiecutterAction({ - // integrations, - // templaters: templatersWithoutCookiecutter, - // reader: mockReader, - // }); - - // await expect(newAction.handler(mockContext)).rejects.toThrow( - // /No templater registered/, - // ); - // }); - - // 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( - // /Relative path is not allowed to refer to a directory outside its parent/, - // ); - // }); + expect(containerRunner.runContainer).toHaveBeenCalledWith( + expect.objectContaining({ + imageName, + }), + ); + }); }); From fd5ec6d304d3f4f6bc0846a8ae8e6e6866402786 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 6 Jul 2021 20:29:58 +0200 Subject: [PATCH 29/47] chore: some api-docs changed Signed-off-by: blam --- .../src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts | 2 -- plugins/sentry/api-report.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts index 8bd205be1d..1ef1e1a940 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts @@ -35,8 +35,6 @@ import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; import { ActionContext } from '../../types'; -import fs from 'fs-extra'; - describe('fetch:cookiecutter', () => { const integrations = ScmIntegrations.fromConfig( new ConfigReader({ diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index c3cce9ed7c..f140fc8b60 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -81,7 +81,7 @@ export type SentryIssue = { // @public (undocumented) export const SentryIssuesWidget: ({ entity, statsFor, variant, }: { entity: Entity; - statsFor?: "12h" | "24h" | undefined; + statsFor?: "24h" | "12h" | undefined; variant?: InfoCardVariants | undefined; }) => JSX.Element; From 4d9264f0fcacff87095e9cb4b81ccb719c5580dd Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 6 Jul 2021 20:37:39 +0200 Subject: [PATCH 30/47] chore: fixing api-reports again Signed-off-by: blam --- plugins/sentry/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index f140fc8b60..c3cce9ed7c 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -81,7 +81,7 @@ export type SentryIssue = { // @public (undocumented) export const SentryIssuesWidget: ({ entity, statsFor, variant, }: { entity: Entity; - statsFor?: "24h" | "12h" | undefined; + statsFor?: "12h" | "24h" | undefined; variant?: InfoCardVariants | undefined; }) => JSX.Element; From 8e183acc9cee145f665b4c0c9d2ca997e2171b50 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 11:02:31 +0200 Subject: [PATCH 31/47] chore: code review comments Signed-off-by: blam --- .../builtin/fetch/cookiecutter.test.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts index 1ef1e1a940..86f953a6a5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts @@ -28,7 +28,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader, JsonObject } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import mock from 'mock-fs'; +import mockFs from 'mock-fs'; import os from 'os'; import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; @@ -94,14 +94,14 @@ describe('fetch:cookiecutter', () => { }; // mock the temp directory - mock({ [mockTmpDir]: {} }); - mock({ [`${join(mockTmpDir, 'template')}`]: {} }); + mockFs({ [mockTmpDir]: {} }); + mockFs({ [`${join(mockTmpDir, 'template')}`]: {} }); commandExists.mockResolvedValue(null); // Mock when run container is called it creates some new files in the mock filesystem containerRunner.runContainer.mockImplementation(async () => { - mock({ + mockFs({ [`${join(mockTmpDir, 'intermediate')}`]: { 'testfile.json': '{}', }, @@ -110,7 +110,7 @@ describe('fetch:cookiecutter', () => { // Mock when runCommand is called it creats some new files in the mock filesystem runCommand.mockImplementation(async () => { - mock({ + mockFs({ [`${join(mockTmpDir, 'intermediate')}`]: { 'testfile.json': '{}', }, @@ -119,26 +119,22 @@ describe('fetch:cookiecutter', () => { }); afterEach(() => { - mock.restore(); + mockFs.restore(); }); it('should throw an error when copyWithoutRender is not an array', async () => { (mockContext.input as any).copyWithoutRender = 'not an array'; - await expect( - action.handler(mockContext), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fetch action input copyWithoutRender must be an Array"`, + await expect(action.handler(mockContext)).rejects.toThrowError( + /Fetch action input copyWithoutRender must be an Array/, ); }); it('should throw an error when extensions is not an array', async () => { (mockContext.input as any).extensions = 'not an array'; - await expect( - action.handler(mockContext), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fetch action input extensions must be an Array"`, + await expect(action.handler(mockContext)).rejects.toThrowError( + /Fetch action input extensions must be an Array/, ); }); From 60e8302222987ae3832efbcb777cba105bb6934c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 11:21:46 +0200 Subject: [PATCH 32/47] chore: made a lovely changeset Signed-off-by: blam --- .changeset/fresh-elephants-tease.md | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .changeset/fresh-elephants-tease.md diff --git a/.changeset/fresh-elephants-tease.md b/.changeset/fresh-elephants-tease.md new file mode 100644 index 0000000000..5141f96be7 --- /dev/null +++ b/.changeset/fresh-elephants-tease.md @@ -0,0 +1,68 @@ +--- +'@backstage/catalog-model': minor +'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +'@backstage/create-app': patch +--- + +Support for `Template` kinds with version `backstage.io/v1alpha1` has now been removed. This means that the old method of running templates with `Preparers`, `Templaters` and `Publishers` has also been removed. If you had any logic in these abstractions, they should now be moved to `actions` instead, and you can find out more about those in the [documentation](https://backstage.io/docs/features/software-templates/writing-custom-actions) + +If you need any help migrating existing templates, there's a [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2). Reach out to us on Discord in the #support channel if you're having problems. + +The `scaffolder-backend` now no longer requires these `Preparers`, `Templaters`, and `Publishers` to be passed in, now all it needs is the `containerRunner`. + +Please update your `packages/backend/src/plugins/scaffolder.ts` like the following + +```diff +- import { +- DockerContainerRunner, +- SingleHostDiscovery, +- } from '@backstage/backend-common'; ++ import { DockerContainerRunner } from '@backstage/backend-common'; + import { CatalogClient } from '@backstage/catalog-client'; +- import { +- CookieCutter, +- CreateReactAppTemplater, +- createRouter, +- Preparers, +- Publishers, +- Templaters, +- } from '@backstage/plugin-scaffolder-backend'; ++ import { createRouter } from '@backstage/plugin-scaffolder-backend'; + import Docker from 'dockerode'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default async function createPlugin({ + config, + database, + reader, ++ discovery, + }: PluginEnvironment): Promise { + const dockerClient = new Docker(); + const containerRunner = new DockerContainerRunner({ dockerClient }); + +- const cookiecutterTemplater = new CookieCutter({ containerRunner }); +- const craTemplater = new CreateReactAppTemplater({ containerRunner }); +- const templaters = new Templaters(); + +- templaters.register('cookiecutter', cookiecutterTemplater); +- templaters.register('cra', craTemplater); +- +- const preparers = await Preparers.fromConfig(config, { logger }); +- const publishers = await Publishers.fromConfig(config, { logger }); + +- const discovery = SingleHostDiscovery.fromConfig(config); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + + return await createRouter({ +- preparers, +- templaters, +- publishers, ++ containerRunner, + logger, + config, + database, + +``` From e922a9952830474e1431330bf46d0a1c601df93d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 11:27:55 +0200 Subject: [PATCH 33/47] chore: updating api-docs Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 377 +---------------------- 1 file changed, 5 insertions(+), 372 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 21c6e03b0f..ab3ea433c6 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -4,18 +4,11 @@ ```ts -import { AzureIntegrationConfig } from '@backstage/integration'; -import { BitbucketIntegrationConfig } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { ContainerRunner } from '@backstage/backend-common'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import express from 'express'; -import { GithubCredentialsProvider } from '@backstage/integration'; -import { GitHubIntegrationConfig } from '@backstage/integration'; -import { Gitlab } from '@gitbeaker/core'; -import { GitLabIntegrationConfig } from '@backstage/integration'; -import gitUrlParse from 'git-url-parse'; import { JsonObject } from '@backstage/config'; import { JsonValue } from '@backstage/config'; import { Logger } from 'winston'; @@ -23,7 +16,6 @@ import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; @@ -40,74 +32,12 @@ export type ActionContext = { createTemporaryDirectory(): Promise; }; -// @public (undocumented) -export class AzurePreparer implements PreparerBase { - constructor(config: { - token?: string; - }); - // (undocumented) - static fromConfig(config: AzureIntegrationConfig): AzurePreparer; - // (undocumented) - prepare({ url, workspacePath, logger }: PreparerOptions): Promise; -} - -// @public (undocumented) -export class AzurePublisher implements PublisherBase { - constructor(config: { - token: string; - }); - // (undocumented) - static fromConfig(config: AzureIntegrationConfig): Promise; - // (undocumented) - publish({ values, workspacePath, logger, }: PublisherOptions): Promise; -} - -// @public (undocumented) -export class BitbucketPreparer implements PreparerBase { - constructor(config: { - username?: string; - token?: string; - appPassword?: string; - }); - // (undocumented) - static fromConfig(config: BitbucketIntegrationConfig): BitbucketPreparer; - // (undocumented) - prepare({ url, workspacePath, logger }: PreparerOptions): Promise; -} - -// @public (undocumented) -export class BitbucketPublisher implements PublisherBase { - constructor(config: { - host: string; - token?: string; - appPassword?: string; - username?: string; - apiBaseUrl?: string; - repoVisibility: RepoVisibilityOptions_2; - }); - // (undocumented) - static fromConfig(config: BitbucketIntegrationConfig, { repoVisibility }: { - repoVisibility: RepoVisibilityOptions_2; - }): Promise; - // (undocumented) - publish({ values, workspacePath, logger, }: PublisherOptions): Promise; -} - // @public export class CatalogEntityClient { constructor(catalogClient: CatalogApi); findTemplate(templateName: string, options?: { token?: string; - }): Promise; -} - -// @public (undocumented) -export class CookieCutter implements TemplaterBase { - constructor({ containerRunner }: { - containerRunner: ContainerRunner; - }); - // (undocumented) - run({ workspacePath, values, logStream, }: TemplaterRunOptions): Promise; + }): Promise; } // @public (undocumented) @@ -115,7 +45,7 @@ export const createBuiltinActions: (options: { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; - templaters: TemplaterBuilder; + containerRunner: ContainerRunner; config: Config; }) => TemplateAction[]; @@ -135,7 +65,7 @@ export function createDebugLogAction(): TemplateAction; export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; - templaters: TemplaterBuilder; + containerRunner: ContainerRunner; }): TemplateAction; // @public (undocumented) @@ -150,9 +80,6 @@ export const createFilesystemDeleteAction: () => TemplateAction; // @public (undocumented) export const createFilesystemRenameAction: () => TemplateAction; -// @public (undocumented) -export function createLegacyActions(options: Options): TemplateAction[]; - // @public (undocumented) export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; @@ -183,15 +110,6 @@ export function createPublishGitlabAction(options: { config: Config; }): TemplateAction; -// @public (undocumented) -export class CreateReactAppTemplater implements TemplaterBase { - constructor({ containerRunner }: { - containerRunner: ContainerRunner; - }); - // (undocumented) - run({ workspacePath, values, logStream, }: TemplaterRunOptions): Promise; -} - // @public (undocumented) export function createRouter(options: RouterOptions): Promise; @@ -200,206 +118,6 @@ export const createTemplateAction: | undefined; }>>(templateAction: TemplateAction) => TemplateAction; -// @public (undocumented) -export class FilePreparer implements PreparerBase { - // (undocumented) - prepare({ url, workspacePath }: PreparerOptions): Promise; -} - -// @public -export const getTemplaterKey: (entity: TemplateEntityV1alpha1) => string; - -// @public (undocumented) -export class GithubPreparer implements PreparerBase { - constructor(config: { - credentialsProvider: GithubCredentialsProvider; - }); - // (undocumented) - static fromConfig(config: GitHubIntegrationConfig): GithubPreparer; - // (undocumented) - prepare({ url, workspacePath, logger }: PreparerOptions): Promise; -} - -// @public @deprecated (undocumented) -export class GithubPublisher implements PublisherBase { - constructor(config: { - credentialsProvider: GithubCredentialsProvider; - repoVisibility: RepoVisibilityOptions; - apiBaseUrl: string | undefined; - }); - // (undocumented) - static fromConfig(config: GitHubIntegrationConfig, { repoVisibility }: { - repoVisibility: RepoVisibilityOptions; - }): Promise; - // (undocumented) - publish({ values, workspacePath, logger, }: PublisherOptions): Promise; -} - -// @public (undocumented) -export class GitlabPreparer implements PreparerBase { - constructor(config: { - token?: string; - }); - // (undocumented) - static fromConfig(config: GitLabIntegrationConfig): GitlabPreparer; - // (undocumented) - prepare({ url, workspacePath, logger }: PreparerOptions): Promise; -} - -// @public (undocumented) -export class GitlabPublisher implements PublisherBase { - constructor(config: { - token: string; - client: Gitlab; - repoVisibility: RepoVisibilityOptions_3; - }); - // (undocumented) - static fromConfig(config: GitLabIntegrationConfig, { repoVisibility }: { - repoVisibility: RepoVisibilityOptions_3; - }): Promise; - // (undocumented) - publish({ values, workspacePath, logger, }: PublisherOptions): Promise; -} - -// @public (undocumented) -export type Job = { - id: string; - context: StageContext; - status: ProcessorStatus; - stages: StageResult[]; - error?: Error; -}; - -// @public (undocumented) -export type JobAndDirectoryTuple = { - job: Job; - directory: string; -}; - -// @public (undocumented) -export class JobProcessor implements Processor { - constructor(workingDirectory: string); - // (undocumented) - create({ entity, values, stages, }: { - entity: TemplateEntityV1alpha1; - values: TemplaterValues; - stages: StageInput[]; - }): Job; - // (undocumented) - static fromConfig({ config, logger, }: { - config: Config; - logger: Logger; - }): Promise; - // (undocumented) - get(id: string): Job | undefined; - // (undocumented) - run(job: Job): Promise; - } - -// @public (undocumented) -export function joinGitUrlPath(repoUrl: string, path?: string): string; - -// @public (undocumented) -export type ParsedLocationAnnotation = { - protocol: 'file' | 'url'; - location: string; -}; - -// @public (undocumented) -export const parseLocationAnnotation: (entity: TemplateEntityV1alpha1) => ParsedLocationAnnotation; - -// @public (undocumented) -export interface PreparerBase { - prepare(opts: PreparerOptions): Promise; -} - -// @public (undocumented) -export type PreparerBuilder = { - register(host: string, preparer: PreparerBase): void; - get(url: string): PreparerBase; -}; - -// @public (undocumented) -export type PreparerOptions = { - url: string; - workspacePath: string; - logger: Logger; -}; - -// @public (undocumented) -export class Preparers implements PreparerBuilder { - // (undocumented) - static fromConfig(config: Config, _: { - logger: Logger; - }): Promise; - // (undocumented) - get(url: string): PreparerBase; - // (undocumented) - register(host: string, preparer: PreparerBase): void; -} - -// @public (undocumented) -export type Processor = { - create({ entity, values, stages, }: { - entity: TemplateEntityV1alpha1; - values: TemplaterValues; - stages: StageInput[]; - }): Job; - get(id: string): Job | undefined; - run(job: Job): Promise; -}; - -// @public (undocumented) -export type ProcessorStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; - -// @public -export type PublisherBase = { - publish(opts: PublisherOptions): Promise; -}; - -// @public (undocumented) -export type PublisherBuilder = { - register(host: string, publisher: PublisherBase): void; - get(storePath: string): PublisherBase; -}; - -// @public (undocumented) -export type PublisherOptions = { - values: TemplaterValues; - workspacePath: string; - logger: Logger; -}; - -// @public (undocumented) -export type PublisherResult = { - remoteUrl: string; - catalogInfoUrl?: string; -}; - -// @public (undocumented) -export class Publishers implements PublisherBuilder { - // (undocumented) - static fromConfig(config: Config, _options: { - logger: Logger; - }): Promise; - // (undocumented) - get(url: string): PublisherBase; - // (undocumented) - register(host: string, preparer: PublisherBase | undefined): void; -} - -// @public (undocumented) -export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; - -// @public -export type RequiredTemplateValues = { - owner: string; - storePath: string; - destination?: { - git?: gitUrlParse.GitUrl; - }; -}; - // @public (undocumented) export interface RouterOptions { // (undocumented) @@ -409,63 +127,17 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) + containerRunner: ContainerRunner; + // (undocumented) database: PluginDatabaseManager; // (undocumented) logger: Logger; // (undocumented) - preparers: PreparerBuilder; - // (undocumented) - publishers: PublisherBuilder; - // (undocumented) reader: UrlReader; // (undocumented) taskWorkers?: number; - // (undocumented) - templaters: TemplaterBuilder; } -// @public (undocumented) -export const runCommand: ({ command, args, logStream, }: RunCommandOptions) => Promise; - -// @public (undocumented) -export type RunCommandOptions = { - command: string; - args: string[]; - logStream?: Writable; -}; - -// @public (undocumented) -export type StageContext = { - values: TemplaterValues; - entity: TemplateEntityV1alpha1; - logger: Logger; - logStream: Writable; - workspacePath: string; -} & T; - -// @public (undocumented) -export interface StageInput { - // (undocumented) - handler(ctx: StageContext): Promise; - // (undocumented) - name: string; -} - -// @public (undocumented) -export interface StageResult extends StageInput { - // (undocumented) - endedAt?: number; - // (undocumented) - log: string[]; - // (undocumented) - startedAt?: number; - // (undocumented) - status: ProcessorStatus; -} - -// @public -export type SupportedTemplatingKey = 'cookiecutter' | string; - // @public (undocumented) export type TemplateAction = { id: string; @@ -487,45 +159,6 @@ export class TemplateActionRegistry { register(action: TemplateAction): void; } -// @public (undocumented) -export type TemplaterBase = { - run(opts: TemplaterRunOptions): Promise; -}; - -// @public -export type TemplaterBuilder = { - register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void; - get(templater: string): TemplaterBase; -}; - -// @public (undocumented) -export type TemplaterConfig = { - templater?: TemplaterBase; -}; - -// @public -export type TemplaterRunOptions = { - workspacePath: string; - values: TemplaterValues; - logStream?: Writable; -}; - -// @public -export type TemplaterRunResult = { - resultDir: string; -}; - -// @public (undocumented) -export class Templaters implements TemplaterBuilder { - // (undocumented) - get(templaterId: string): TemplaterBase; - // (undocumented) - register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase): void; - } - -// @public (undocumented) -export type TemplaterValues = RequiredTemplateValues & Record; - // (No @packageDocumentation comment for this package) From 0adfae5c812655cf886dda0140e4a04d77aeaf80 Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Wed, 7 Jul 2021 21:37:52 +1200 Subject: [PATCH 34/47] added changeset Signed-off-by: Jos Craw --- .changeset/red-ladybugs-promise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-ladybugs-promise.md diff --git a/.changeset/red-ladybugs-promise.md b/.changeset/red-ladybugs-promise.md new file mode 100644 index 0000000000..dbfbf2bbd4 --- /dev/null +++ b/.changeset/red-ladybugs-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +add support for uiSchema on dependent form fields From 7a3ad92b525354993908d52af27c9f7984085070 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Fri, 11 Jun 2021 11:01:53 -0300 Subject: [PATCH 35/47] feat: added rails templater to scaffolder-backend Signed-off-by: Rogerio Angeliski --- .changeset/twelve-deers-explode.md | 5 + plugins/scaffolder-backend/package.json | 6 +- .../sample-templates/local-templates.yaml | 1 + .../sample-templates/rails-demo/template.yaml | 174 ++++++++++++ .../template/rails-template-file.rb | 14 + .../scripts/Rails.dockerfile | 9 + .../actions/builtin/createBuiltinActions.ts | 10 +- .../scaffolder/actions/builtin/fetch/index.ts | 1 + .../actions/builtin/fetch/rails.test.ts | 140 ++++++++++ .../scaffolder/actions/builtin/fetch/rails.ts | 182 +++++++++++++ .../src/scaffolder/stages/templater/index.ts | 21 ++ .../stages/templater/rails/index.test.ts | 257 ++++++++++++++++++ .../stages/templater/rails/index.ts | 96 +++++++ .../rails/railsArgumentResolver.test.ts | 50 ++++ .../templater/rails/railsArgumentResolver.ts | 106 ++++++++ 15 files changed, 1070 insertions(+), 2 deletions(-) create mode 100644 .changeset/twelve-deers-explode.md create mode 100644 plugins/scaffolder-backend/sample-templates/rails-demo/template.yaml create mode 100644 plugins/scaffolder-backend/sample-templates/rails-demo/template/rails-template-file.rb create mode 100644 plugins/scaffolder-backend/scripts/Rails.dockerfile create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.ts diff --git a/.changeset/twelve-deers-explode.md b/.changeset/twelve-deers-explode.md new file mode 100644 index 0000000000..c5ce1318a6 --- /dev/null +++ b/.changeset/twelve-deers-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Created a Rails templater to scaffold apps diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7173f865c5..0bc4c62ea6 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -40,6 +40,9 @@ "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", "azure-devops-node-api": "^10.2.2", + "@types/git-url-parse": "^9.0.0", + "@types/dockerode": "^3.2.1", + "azure-devops-node-api": "^10.1.1", "command-exists": "^1.2.9", "compression": "^1.7.4", "cors": "^2.8.5", @@ -60,7 +63,8 @@ "octokit-plugin-create-pull-request": "^3.9.3", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0" + "yaml": "^1.10.0", + "dockerode": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.7.3", diff --git a/plugins/scaffolder-backend/sample-templates/local-templates.yaml b/plugins/scaffolder-backend/sample-templates/local-templates.yaml index 3b3acbae7e..59a55b3cd9 100644 --- a/plugins/scaffolder-backend/sample-templates/local-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/local-templates.yaml @@ -10,4 +10,5 @@ spec: - ./create-react-app/template.yaml - ./springboot-grpc-template/template.yaml - ./v1beta2-demo/template.yaml + - ./rails-demo/template.yaml - ./pull-request/template.yaml 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/scripts/Rails.dockerfile b/plugins/scaffolder-backend/scripts/Rails.dockerfile new file mode 100644 index 0000000000..3cec0e7668 --- /dev/null +++ b/plugins/scaffolder-backend/scripts/Rails.dockerfile @@ -0,0 +1,9 @@ +FROM ruby:3.0 + +RUN apt-get update -qq && \ + apt-get install -y \ + nodejs \ + postgresql-client \ + git + +RUN gem install rails diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 08d7bbf824..fa2fcbfcee 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -24,7 +24,11 @@ import { } from './catalog'; import { createDebugLogAction } from './debug'; -import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch'; +import { + createFetchCookiecutterAction, + createFetchPlainAction, + createFetchRailsAction, +} from './fetch'; import { createFilesystemDeleteAction, createFilesystemRenameAction, @@ -62,6 +66,10 @@ export const createBuiltinActions = (options: { integrations, containerRunner, }), + createFetchRailsAction({ + reader, + integrations, + }), createPublishGithubAction({ integrations, config, 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..9557a88a4c 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 { createFetchRailsAction } from './rails'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts new file mode 100644 index 0000000000..ebc62e9277 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const mockRailsTemplater = { run: jest.fn() }; +jest.mock('./helpers'); +jest.mock('../../../stages/templater', () => { + return { + Rails: jest.fn().mockImplementation(() => { + return mockRailsTemplater; + }), + }; +}); + +import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import mock from 'mock-fs'; +import os from 'os'; +import { resolve as resolvePath } from 'path'; +import { PassThrough } from 'stream'; +import { createFetchRailsAction } from './rails'; +import { fetchContents } from './helpers'; + +describe('fetch:rails', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }), + ); + + const mockTmpDir = os.tmpdir(); + const mockContext = { + input: { + url: 'https://rubyonrails.org/generator', + targetPath: 'something', + values: { + help: 'me', + }, + }, + baseUrl: 'somebase', + workspacePath: mockTmpDir, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + + const mockReader: UrlReader = { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const action = createFetchRailsAction({ + integrations, + reader: mockReader, + }); + + beforeEach(() => { + mock({ [`${mockContext.workspacePath}/result`]: {} }); + jest.restoreAllMocks(); + }); + + afterEach(() => { + mock.restore(); + }); + + it('should call fetchContents with the correct values', async () => { + await action.handler(mockContext); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it('should execute the rails templater with the correct values', async () => { + await action.handler(mockContext); + + expect(mockRailsTemplater.run).toHaveBeenCalledWith({ + workspacePath: mockTmpDir, + logStream: mockContext.logStream, + values: mockContext.input.values, + }); + }); + + it('should execute the rails templater with optional inputs if they are present and valid', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + imageName: 'foo/rails-custom-image', + }, + }); + + expect(mockRailsTemplater.run).toHaveBeenCalledWith({ + workspacePath: mockTmpDir, + logStream: mockContext.logStream, + values: { + ...mockContext.input.values, + imageName: 'foo/rails-custom-image', + }, + }); + }); + + it('should throw if the target directory is outside of the workspace path', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + targetPath: '/foo', + }, + }), + ).rejects.toThrow( + /targetPath may not specify a path outside the working directory/, + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts new file mode 100644 index 0000000000..203db5004e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts @@ -0,0 +1,182 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DockerContainerRunner, UrlReader } from '@backstage/backend-common'; +import { JsonObject } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { ScmIntegrations } from '@backstage/integration'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { Rails, TemplaterValues } from '../../../stages/templater'; +import { createTemplateAction } from '../../createTemplateAction'; +import { fetchContents } from './helpers'; +import Docker from 'dockerode'; + +export function createFetchRailsAction(options: { + reader: UrlReader; + integrations: ScmIntegrations; +}) { + const { reader, integrations } = options; + + return createTemplateAction<{ + url: string; + targetPath?: string; + values: JsonObject; + imageName?: string; + }>({ + id: 'fetch:rails', + description: + 'Downloads a template from the given URL into the workspace, and runs a rails generator on it.', + schema: { + input: { + type: 'object', + required: ['url'], + properties: { + url: { + title: 'Fetch URL', + description: + 'Relative path or absolute URL pointing to the directory tree to fetch', + type: 'string', + }, + targetPath: { + title: 'Target Path', + description: + 'Target path within the working directory to download the contents to.', + type: 'string', + }, + values: { + title: 'Template Values', + description: 'Values to pass on to rails for templating', + type: 'object', + properties: { + railsArguments: { + title: 'Arguments to pass to new command', + description: + 'You can provide some arguments to create a custom app', + type: 'object', + properties: { + minimal: { + title: 'minimal', + description: 'Preconfigure a minimal rails app', + type: 'boolean', + }, + skipBundle: { + title: 'skipBundle', + description: "Don't run bundle install", + type: 'boolean', + }, + skipWebpackInstall: { + title: 'skipWebpackInstall', + description: "Don't run Webpack install", + type: 'boolean', + }, + api: { + title: 'api', + description: 'Preconfigure smaller stack for API only apps', + type: 'boolean', + }, + template: { + title: 'template', + description: + 'Path to some application template (can be a filesystem path or URL)', + type: 'string', + }, + webpacker: { + title: 'webpacker', + description: + 'Preconfigure Webpack with a particular framework (options: react, vue, angular, elm, stimulus)', + type: 'string', + enum: ['react', 'vue', 'angular', 'elm', 'stimulus'], + }, + database: { + title: 'database', + description: + 'Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)', + type: 'string', + enum: [ + 'mysql', + 'postgresql', + 'sqlite3', + 'oracle', + 'sqlserver', + 'jdbcmysql', + 'jdbcsqlite3', + 'jdbcpostgresql', + 'jdbc', + ], + }, + railsVersion: { + title: 'Rails version in Gemfile', + description: + 'Set up the application with Gemfile pointing to a specific version (options: fromImage, dev, edge, master)', + type: 'string', + enum: ['dev', 'edge', 'master', 'fromImage'], + }, + }, + }, + }, + }, + imageName: { + title: 'Rails Docker image', + description: + 'Specify a Docker image to run rails new. Used only when a local rails is not found.', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info('Fetching and then templating using rails'); + + const workDir = await ctx.createTemporaryDirectory(); + const resultDir = resolvePath(workDir, 'result'); + + await fetchContents({ + reader, + integrations, + baseUrl: ctx.baseUrl, + fetchUrl: ctx.input.url, + outputPath: workDir, + }); + + const dockerClient = new Docker(); + const containerRunner = new DockerContainerRunner({ dockerClient }); + const templateRunner = new Rails({ containerRunner }); + + const values = { + ...(ctx.input.values as TemplaterValues), + imageName: ctx.input.imageName, + }; + + // Will execute the template in ./template and put the result in ./result + await templateRunner.run({ + workspacePath: workDir, + logStream: ctx.logStream, + values, + }); + + // Finally move the template result into the task workspace + const targetPath = ctx.input.targetPath ?? './'; + const outputPath = resolvePath(ctx.workspacePath, targetPath); + if (!outputPath.startsWith(ctx.workspacePath)) { + throw new InputError( + `Fetch action targetPath may not specify a path outside the working directory`, + ); + } + await fs.copy(resultDir, outputPath); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts new file mode 100644 index 0000000000..139ff2207e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './cookiecutter'; +export * from './types'; +export * from './helpers'; +export * from './templaters'; +export * from './cra'; +export * from './rails'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts new file mode 100644 index 0000000000..534899eec1 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const runCommand = jest.fn(); +const commandExists = jest.fn(); + +jest.mock('../helpers', () => ({ runCommand })); +jest.mock('command-exists', () => commandExists); +jest.mock('fs-extra'); + +import { ContainerRunner } from '@backstage/backend-common'; +import fs from 'fs-extra'; + +import path from 'path'; +import { PassThrough } from 'stream'; + +import { Rails } from './index'; + +describe('Rails Templater', () => { + const containerRunner: jest.Mocked = { + runContainer: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when running on docker', () => { + it('should run the correct bindings for the volumes', async () => { + const values = { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + imageName: 'foo/rails-custom-image', + }; + + jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + jest + .spyOn(fs, 'realpath') + .mockImplementation(x => Promise.resolve(x.toString())); + + const templater = new Rails({ containerRunner }); + await templater.run({ + workspacePath: 'tempdir', + values, + }); + + expect(containerRunner.runContainer).toHaveBeenCalledWith({ + imageName: 'foo/rails-custom-image', + command: 'rails', + args: ['new', '/output/rails-project'], + envVars: { HOME: '/tmp' }, + mountDirs: { + ['tempdir']: '/input', + [path.join('tempdir', 'intermediate')]: '/output', + }, + workingDir: '/input', + logStream: undefined, + }); + }); + + it('should use the provided imageName', async () => { + const values = { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + imageName: 'foo/rails-custom-image', + }; + + jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + + const templater = new Rails({ containerRunner }); + await templater.run({ + workspacePath: 'tempdir', + values, + }); + + expect(containerRunner.runContainer).toHaveBeenCalledWith( + expect.objectContaining({ + imageName: 'foo/rails-custom-image', + }), + ); + }); + + it('should pass through the streamer to the run docker helper', async () => { + const stream = new PassThrough(); + + const values = { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + imageName: 'foo/rails-custom-image', + }; + + jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + + const templater = new Rails({ containerRunner }); + await templater.run({ + workspacePath: 'tempdir', + values, + logStream: stream, + }); + + expect(containerRunner.runContainer).toHaveBeenCalledWith({ + imageName: 'foo/rails-custom-image', + command: 'rails', + args: ['new', '/output/rails-project'], + envVars: { HOME: '/tmp' }, + mountDirs: { + ['tempdir']: '/input', + [path.join('tempdir', 'intermediate')]: '/output', + }, + workingDir: '/input', + logStream: stream, + }); + }); + + it('update the template path to correct location', async () => { + const values = { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + railsArguments: { template: './something.rb' }, + imageName: 'foo/rails-custom-image', + }; + + jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + jest + .spyOn(fs, 'realpath') + .mockImplementation(x => Promise.resolve(x.toString())); + + const templater = new Rails({ containerRunner }); + await templater.run({ + workspacePath: 'tempdir', + values, + }); + + expect(containerRunner.runContainer).toHaveBeenCalledWith({ + imageName: 'foo/rails-custom-image', + command: 'rails', + args: [ + 'new', + '/output/rails-project', + '--template', + '/input/something.rb', + ], + envVars: { HOME: '/tmp' }, + mountDirs: { + ['tempdir']: '/input', + [path.join('tempdir', 'intermediate')]: '/output', + }, + workingDir: '/input', + logStream: undefined, + }); + }); + }); + + describe('when rails is available', () => { + it('use the binary', async () => { + const stream = new PassThrough(); + + const values = { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + imageName: 'foo/rails-custom-image', + }; + + jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + commandExists.mockImplementationOnce(() => () => true); + + const templater = new Rails({ containerRunner }); + await templater.run({ + workspacePath: 'tempdir', + values, + logStream: stream, + }); + + expect(runCommand).toHaveBeenCalledWith({ + command: 'rails', + args: expect.arrayContaining([ + 'new', + path.join('tempdir', 'intermediate', 'rails-project'), + ]), + logStream: stream, + }); + }); + it('update the template path to correct location', async () => { + const stream = new PassThrough(); + + const values = { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + railsArguments: { template: './something.rb' }, + imageName: 'foo/rails-custom-image', + }; + + jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + commandExists.mockImplementationOnce(() => () => true); + + const templater = new Rails({ containerRunner }); + await templater.run({ + workspacePath: 'tempdir', + values, + logStream: stream, + }); + + expect(runCommand).toHaveBeenCalledWith({ + command: 'rails', + args: expect.arrayContaining([ + 'new', + path.join('tempdir', 'intermediate', 'rails-project'), + '--template', + path.join('tempdir', './something.rb'), + ]), + logStream: stream, + }); + }); + }); + + describe('when nothing was generated', () => { + it('throws an error', async () => { + const stream = new PassThrough(); + + jest + .spyOn(fs, 'readdir') + .mockImplementationOnce(() => Promise.resolve([])); + + const templater = new Rails({ containerRunner }); + await expect( + templater.run({ + workspacePath: 'tempdir', + values: { + owner: 'angeliski', + storePath: 'https://github.com/angeliski/rails-project', + name: 'rails-project', + imageName: 'foo/rails-custom-image', + }, + logStream: stream, + }), + ).rejects.toThrow(/No data generated by rails/); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts new file mode 100644 index 0000000000..57ee26e106 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ContainerRunner } from '@backstage/backend-common'; +import fs from 'fs-extra'; +import path from 'path'; +import { runCommand } from '../helpers'; +import commandExists from 'command-exists'; +import { TemplaterBase, TemplaterRunOptions } from '../types'; +import { railsArgumentResolver } from './railsArgumentResolver'; + +export class Rails implements TemplaterBase { + private readonly containerRunner: ContainerRunner; + + constructor({ containerRunner }: { containerRunner: ContainerRunner }) { + this.containerRunner = containerRunner; + } + + public async run({ + workspacePath, + values, + logStream, + }: TemplaterRunOptions): Promise { + const intermediateDir = path.join(workspacePath, 'intermediate'); + await fs.ensureDir(intermediateDir); + const resultDir = path.join(workspacePath, 'result'); + + const { name, imageName, railsArguments } = values; + + // Directories to bind on container + const mountDirs = { + [workspacePath]: '/input', + [intermediateDir]: '/output', + }; + + const baseCommand = 'rails'; + const baseArguments = ['new']; + const commandExistsToRun = await commandExists(baseCommand); + + if (commandExistsToRun) { + const arrayExtraArguments = railsArgumentResolver( + workspacePath, + railsArguments, + ); + + await runCommand({ + command: baseCommand, + args: [ + ...baseArguments, + `${intermediateDir}/${name}`, + ...arrayExtraArguments, + ], + logStream, + }); + } else { + const arrayExtraArguments = railsArgumentResolver( + '/input', + railsArguments, + ); + await this.containerRunner.runContainer({ + imageName: imageName, + command: baseCommand, + args: [...baseArguments, `/output/${name}`, ...arrayExtraArguments], + mountDirs, + workingDir: '/input', + // Set the home directory inside the container as something that applications can + // write to, otherwise they will just fail trying to write to / + envVars: { HOME: '/tmp' }, + logStream, + }); + } + + // if command was successful, intermediateDir should contain + // exactly one directory. + const [generated] = await fs.readdir(intermediateDir); + + if (generated === undefined) { + throw new Error(`No data generated by ${baseCommand}`); + } + + await fs.move(path.join(intermediateDir, generated), resultDir); + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts new file mode 100644 index 0000000000..370d9de380 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { railsArgumentResolver } from './railsArgumentResolver'; +import { TemplaterValues } from '../types'; + +describe('railsArgumentResolver', () => { + describe('when provide the parameter', () => { + test.each([ + [{}, []], + [{ minimal: true }, ['--minimal']], + [{ api: true }, ['--api']], + [{ skipBundle: true }, ['--skip-bundle']], + [{ skipWebpackInstall: true }, ['--skip-webpack-install']], + [{ webpacker: 'vue' }, ['--webpack', 'vue']], + [{ database: 'postgresql' }, ['--database', 'postgresql']], + [{ railsVersion: 'dev' }, ['--dev']], + [{ template: './rails.rb' }, ['--template', '/tmp/rails.rb']], + ])( + 'should include the argument to execution %p -> %p', + (passedArguments: object, expected: Array) => { + // that step is to ensure the validation between the TemplaterValues and the resolver + const values: TemplaterValues = { + owner: 'r', + storePath: '', + railsArguments: passedArguments, + }; + + const { railsArguments } = values; + + const argumentsToRun = railsArgumentResolver('/tmp', railsArguments); + + expect(argumentsToRun).toEqual(expected); + }, + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.ts new file mode 100644 index 0000000000..5ea2f70895 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum Webpacker { + react = 'react', + vue = 'vue', + angular = 'angular', + elm = 'elm', + stimulus = 'stimulus', +} + +enum Database { + mysql = 'mysql', + postgresql = 'postgresql', + sqlite3 = 'sqlite3', + oracle = 'oracle', + sqlserver = 'sqlserver', + jdbcmysql = 'jdbcmysql', + jdbcsqlite3 = 'jdbcsqlite3', + jdbcpostgresql = 'jdbcpostgresql', + jdbc = 'jdbc', +} + +enum RailsVersion { + dev = 'dev', + edge = 'edge', + master = 'master', + fromImage = 'fromImage', +} + +export type RailsRunOptions = { + minimal?: boolean; + api?: boolean; + template?: string; + webpacker?: Webpacker; + database?: Database; + railsVersion?: RailsVersion; + skipBundle?: boolean; + skipWebpackInstall?: boolean; +}; + +export const railsArgumentResolver = ( + projectRoot: string, + options: RailsRunOptions, +): string[] => { + const argumentsToRun: string[] = []; + + if (options?.minimal) { + argumentsToRun.push('--minimal'); + } + + if (options?.api) { + argumentsToRun.push('--api'); + } + + if (options?.skipBundle) { + argumentsToRun.push('--skip-bundle'); + } + + if (options?.skipWebpackInstall) { + argumentsToRun.push('--skip-webpack-install'); + } + + if ( + options?.webpacker && + Object.values(Webpacker).includes(options?.webpacker as Webpacker) + ) { + argumentsToRun.push('--webpack'); + argumentsToRun.push(options.webpacker); + } + + if ( + options?.database && + Object.values(Database).includes(options?.database as Database) + ) { + argumentsToRun.push('--database'); + argumentsToRun.push(options.database); + } + + if ( + options?.railsVersion !== RailsVersion.fromImage && + Object.values(RailsVersion).includes(options?.railsVersion as RailsVersion) + ) { + argumentsToRun.push(`--${options.railsVersion}`); + } + + if (options?.template) { + argumentsToRun.push('--template'); + argumentsToRun.push(options.template.replace('./', `${projectRoot}/`)); + } + + return argumentsToRun; +}; From aad73cf398fcc63ab4e182b3a38fd3b422da3cb9 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Fri, 18 Jun 2021 11:54:19 -0300 Subject: [PATCH 36/47] fix: move dep to correction location Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 0bc4c62ea6..a6db486594 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -41,7 +41,6 @@ "@types/express": "^4.17.6", "azure-devops-node-api": "^10.2.2", "@types/git-url-parse": "^9.0.0", - "@types/dockerode": "^3.2.1", "azure-devops-node-api": "^10.1.1", "command-exists": "^1.2.9", "compression": "^1.7.4", @@ -74,6 +73,7 @@ "@types/git-url-parse": "^9.0.0", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", + "@types/dockerode": "^3.2.1", "jest-when": "^3.1.0", "mock-fs": "^4.13.0", "msw": "^0.29.0", From c5e81eab43c5ecf5774d0780561867caff34f4bd Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 24 Jun 2021 12:00:43 -0300 Subject: [PATCH 37/47] refactor to a isolated module Signed-off-by: Rogerio Angeliski --- .changeset/twelve-deers-explode.md | 2 +- .../scaffolder-backend-module-rails.yaml | 9 + microsite/static/img/rails-icon.png | Bin 0 -> 5439 bytes packages/backend/package.json | 2 + .../.eslintrc.js | 3 + .../scaffolder-backend-module-rails/README.md | 225 ++++++++++++++++++ .../Rails.dockerfile | 0 .../api-report.md | 22 ++ .../package.json | 44 ++++ .../src/actions/fetch/index.ts | 16 ++ .../src/actions/fetch/rails/index.test.ts} | 23 +- .../src/actions/fetch/rails/index.ts} | 23 +- .../rails/railsArgumentResolver.test.ts | 3 +- .../fetch}/rails/railsArgumentResolver.ts | 0 .../fetch/rails/railsNewRunner.test.ts} | 28 ++- .../actions/fetch/rails/railsNewRunner.ts} | 24 +- .../src/actions/index.ts | 16 ++ .../src/index.ts | 16 ++ plugins/scaffolder-backend/api-report.md | 6 +- plugins/scaffolder-backend/package.json | 4 +- .../actions/builtin/createBuiltinActions.ts | 10 +- .../scaffolder/actions/builtin/fetch/index.ts | 2 +- .../src/scaffolder/stages/templater/index.ts | 1 - 23 files changed, 423 insertions(+), 56 deletions(-) create mode 100644 microsite/data/plugins/scaffolder-backend-module-rails.yaml create mode 100644 microsite/static/img/rails-icon.png create mode 100644 plugins/scaffolder-backend-module-rails/.eslintrc.js create mode 100644 plugins/scaffolder-backend-module-rails/README.md rename plugins/{scaffolder-backend/scripts => scaffolder-backend-module-rails}/Rails.dockerfile (100%) create mode 100644 plugins/scaffolder-backend-module-rails/api-report.md create mode 100644 plugins/scaffolder-backend-module-rails/package.json create mode 100644 plugins/scaffolder-backend-module-rails/src/actions/fetch/index.ts rename plugins/{scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts => scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts} (86%) rename plugins/{scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts => scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts} (90%) rename plugins/{scaffolder-backend/src/scaffolder/stages/templater => scaffolder-backend-module-rails/src/actions/fetch}/rails/railsArgumentResolver.test.ts (95%) rename plugins/{scaffolder-backend/src/scaffolder/stages/templater => scaffolder-backend-module-rails/src/actions/fetch}/rails/railsArgumentResolver.ts (100%) rename plugins/{scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts => scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts} (89%) rename plugins/{scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts => scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts} (84%) create mode 100644 plugins/scaffolder-backend-module-rails/src/actions/index.ts create mode 100644 plugins/scaffolder-backend-module-rails/src/index.ts diff --git a/.changeset/twelve-deers-explode.md b/.changeset/twelve-deers-explode.md index c5ce1318a6..0d34392eab 100644 --- a/.changeset/twelve-deers-explode.md +++ b/.changeset/twelve-deers-explode.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': patch --- -Created a Rails templater to scaffold apps +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 0000000000000000000000000000000000000000..f7b8b69bd9695a8dc98b7bb4b5f848e2ab6eccdf GIT binary patch literal 5439 zcmV-F6~O9=P){007Ad0{{R3(jK%j0002AP)t-s|NsBZ zASdwf@bB;M`T6g=R z(K9;PZ*tXRXV`Uk?A_h`^787kv+B{()naAu#m3JpGS4S2PzMoZgscI3Rh=cT68 zQ&!=bo8qdh+>ep?m_NS>E5&j@ji3TJ1-Z@WWFY|WHK25V6E9uRkfk9t`>9w@3whYWv#`# zkaZ#Nw)n@q+vX1^Q)}+2sRA_^Q1eNsMM#ujNK2*$ezE^z{~u$IJ|_;>M7gOXg(8Wp zmDs39vow-5ONiPp_WvjL!^(R|yaEy+AfW}{i^=<;8PaG2O%+0@YP|7oGw8P2BLK2) zYu|;CcUwWXy(f`CjXx2g1{pN~CZDU<*RvtmlK|-?q71<}Luvv;S_(CgyAs<$H@*@Z zNh`5{eOG;pyhI2&mW6tGi#gHRN@1w5eU}*O0Dj#NnF?*@6DJW>3;(!J`>r$W3cCDe z?9u1a)_mB7thDCAnWej~Yw$&%{`v^aT=Nlz3a@#t0NPwzczS_%JzdAU?V#J@-BzRv zb4jFA16#X$`}gl?umK4qc3ph2@;I%FA&?5|APu;K6uF|kWf-we*;cz&f()PI>4i|I@U^>BJ5(|S_1Jg#$pM;%&GZT%Q ziP*u-dh{@Rw5f?l7H#bR#%y*wI(wb8LktZa&k~w7&-)?e9pdTCG4=D78X`g8i1ugG zMUHLgrpaOL8vUnh*bXwTH&fY!nT8B-m z;UbfvJWRXb&0*~O2l9ZR?T&=1fvY=2VPpZ{zW~=l=e;q(1QM_~J4bz7uYFIJo0|N( zt~a^*%%cYJXU}omLcsl|8_1czs+dt5e~@wveTz@KRBV+U`JqNCA*fBgD_TxsAwd;>kcEs9{zbFvfGyz0p!doJk4b|0D81v@5? z=%e>fZ^6ExOzoBn-$r+1rua)Rq($NR*bnQ|lI@algg@V_A?N5_bbeKpp`-h>lX-RI zyy=d8u>h(pL*E#yd^3z%kh5mmc5i~=55a!fcjR?@7J35>CRnWl`vK%#I8-5^SJQG2 zp|^Sy8HUp2y?w|AR*!bAlfFl9ylGlx?u7RTg7>FI-(Mp#p~691YQ9|VA6;M;)ckiTAwPw#f~9@o6Eis1k%wN zPG%?(lfUMZFq$`E-)%-$PDG)B!B zTF!Qg)uh($h<+W1t|-ofHx!)AWX-x^(=Cb5?X~x1=TDTO#$>V`FE?2>J+b;`B0!|(!p_}Md;C9NDr0Lwn8B((t!s|!NLvJW6ERVgEyg#EohAS$T z(zC8RSK>ApA|8>Of5|dL$c5o)J=0lZwXnQrq%vgXFYs>P_CcUEZUe0^3Eeoq$6_`F zxVBMu3Nevq2xvVo_-``hQxH?gzN;ZynD5(=zuv?<&zQc-j-;AHw}gbGOIu!F#!f*p zPyPgkz-Px(dR7NSj;`XEYVc`Qy6Wk8_M8eCxU=)XhMI{!a743I%PEC;s_?gOJWj|_ zeA+28-FLzqSvhacL`#r-QdAaugKEtI-$UsJ)p)W#t?*o-HP5&mJIVyA_(RTzH_@N0 zT;ie`oKc4G<;>zP!YfW#^TF%$slmh))#8b=V0%gEa+8M+G^W(znY+AOHgi|=!lS0A ze6ulmZ=_P(hE2>?a?^^Y)H}uV;f`9-bn7Bc_Ue%-9_nZx+NbrFQL|VcMF9N*^Q~?t zyb|llDJ5_~XTn4`WKZpovC0=Y!|wF3ryQxiJH#C^{AJks7dS?o7LVjqywRZ~@Wx@` zrr}}k$1VOYjuNLOvU*OT`i!jkm)uU~$zM5!itk~o-dfYPl7}y7g(<;QMak zRgYQCq^BcD`5X48IwgT=u!JZiau+=I(^#XV%IdOL_s>!1H7X)u#J28gYUrzOLE1VPxzmy`5@`lP-!RsNX-ZEA$jI#8r8h#+Fjq$Hcp+$9+vX;)Nv6e)|k6tdE{K;S8%S!=?gWZo)q?5?dLWTDT-D!Jj zC*{{=VGm`!kqhZ**Iz(`oY{&o+nyx$5G8S-nh)fa88p|j2RDT8 z6y&8{LG*iLZWm2^qWZJ(-8L@Yw^8wf=f0W-QYLg6Egfm-`U`G=H@;l-K0X!SrU3T) zy&pr3H2w5Re%g&W1$@U}p)vSD*l}+koHtGrI54;S^+=L3R6E z0^>E@%sEcY2mHBlm-H)T3>^1pL)4n49YEeb5^-N!yy{TosA&PL2XIq&F-3hU1&D4CN#!Sw5rH~^DfA1xh3zp zv@zp+Qhr??cKo;GrS^XSyIRVhJ$>v2jP(20WMas zAxc7%CsDlS11W9cXhV#I|L)xP!ai*#Lr3&!%WmBR?B^bK7O##<;fecoC8Je7c6I~u z!6RVWiyw!*w1s2XS*&vIFC27vV%YP<3$7}NyBDYZ@UdIVQ<3+g^%2TjcZHq3Vo0w1 z6CGoKa}z2`7NN*b&nQ*%VP2)itTCc4&G5nD$2H zpzZC9wF};&-M6577VW;ByUx#V#{c2ID7*{RrYE zP&*NyHsi|K_uN{mmqZq(y6coSQUgvhXV5>S<^w1yg(rCa{)_$pi9Ofbl1g)ERB_mT zR?P=_8}}1^+Kk2g<9(Z7?Ehcv(dR;ULQ|cVxW|z)RIbNyvNa#t$MQ6f=lT}Vz7y6K zyhXacl}I)zg#GfZztzS2Zdids|5NW*HN1%-Sp1%vZFE&ONtD__TT{m2~9%y333x zrEZ|}WwZn`r*_pxw#$ALM`*5Lq5hf`V)n6ugr1C*K);1RTM8g zG&2ryOtjgZ%Sd34%lq&#{_NaT*%RUxP#V+O37Q=3$E!+)N;F`3(!PT%2 zbC{CGUVhEf!ESejD2r6@G@oF#HQ&n-t72XJn1^v)v3%0aEx-PK@ zMP7OJ6E;iMLa4`3O5&;9^AhH9_*0K?cdF+>z9 z+i0)(<}{3bs0QXVg1w4;4`V-A&4&c+d$|c+ppnI1#jg{wzlD+^pr97&^=Tv6=ih~0 zkQM#2hOFR9l5~tPOU5l(b?$RC&d)^QF*n{K5X}x#S^+-hIed5apYtu^D z!*N;_&ol3FnCP>afvI54hh{PC=*BDS&N*8W&n76@42n$avL7>D35WIiX|3X?0_`ccYGnU4{5-FykR4#|tNk z)SN%Zz;0USL+~BgJr{30zG3ZkJRXa>hSk3=h&@#{)KxYZ(?-ub?HiZ(bnHp;jwR$> zuup_Q$P?8yVQh*$SKgEDyI@W;=iNk)&qM4_0f^c6bnMK=sYPrYjNA9XQ4jA!pcp6h zO8r-?`H+R(oDCx@vEX&-s=iHfT231FxNp-aaH8N3xaQ4Sv_1Z%?kLN+W4rODhb+WR^_mZG6^dPeK>~PhC04g_ zai2Ec+&ebfb?)t4v{mc%X<5uFzJcQ4lYJXUNJtBvNfq>MN@J(~d|(Rei__l5VkS)C zC1SU}?pfOoS-U6^uPcq63eX{9)c7uz?|}x%*wL2oEFVrSw0}P|cC^(BV^5V0vWjAd zuoDO0OWecyLg&Y6L)Z^1@5a22nh%jUt;+Hj5;CbMcut}f((~vN?9)1;aT}`Vaa7N< z@y@eTDkyeJD+jx&tF_d8NQ_Z;lfZ*`%<7Yq;N~=Rjl8IRPr!alMG$H z%aV|l_;~j|_+>NlNBGP82i0|I=%dd28-MsVPFHO2BTa>Pko?E%(UhNz)m8Jsvj@UH zO1O@O!}};qg0BCmK!S+Rk^~Z3z(r((78;`+{hrumKYa9iKkoOA*e~{yKJ9PKhu@kH pzcn99O5q9K{r|=Oi~WCw{XY#6v!hb+MnC`n002ovPDHLkV1m;~1}Xpm literal 0 HcmV?d00001 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/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..3c0c9da53e --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -0,0 +1,225 @@ +# scaffolder-backend-module-rails + +Welcome to the Rails Module for Scaffolder. + +Here you can find all Rails related features to improve your scaffolder: + +- Rails Action to use the `new` command +- More features are coming + +## Getting started + +You need to configure the action in your backend: + +``` +yarn add @backstage/plugin-scaffolder-backend-module-rails +``` + +Configure the action (you can check +the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to +see all options): + +```typescript +const actions = [ + createFetchRailsAction({ + integrations, + reader, + containerRunner, + }), +]; + +return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + database, + catalogClient, + reader, + actions, +}); +``` + +After that you can use the action in your template: + +```yaml +apiVersion: backstage.io/v1beta2 +kind: Template +metadata: + name: rails-demo + title: Rails template + description: scaffolder Rails app +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Fill in some steps + required: + - name + - owner + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + allowedKinds: + - Group + system: + title: System + type: string + description: System of the component + ui:field: EntityPicker + ui:options: + allowedKinds: + - System + defaultKind: System + + - title: Choose a location + required: + - repoUrl + - dryRun + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + dryRun: + title: Only perform a dry run, don't publish anything + type: boolean + default: false + railsArguments: + title: arguments to run the rails new command + type: object + properties: + minimal: + title: minimal + description: Preconfigure a minimal rails app + type: boolean + skipBundle: + title: skipBundle + description: Don't run bundle install + type: boolean + skipWebpackInstall: + title: skipWebpackInstall + description: Don't run Webpack install + type: boolean + api: + title: api + description: Preconfigure smaller stack for API only apps + type: boolean + template: + title: template + description: Path to some application template (can be a filesystem path or URL) + type: string + default: './rails-template-file.rb' + webpacker: + title: webpacker + description: + 'Preconfigure Webpack with a particular framework (options: react, + vue, angular, elm, stimulus)' + type: string + enum: + - react + - vue + - angular + - elm + - stimulus + database: + title: database + description: 'Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)' + type: string + enum: + - mysql + - postgresql + - sqlite3 + - oracle + - sqlserver + - jdbcmysql + - jdbcsqlite3 + - jdbcpostgresql + - jdbc + railsVersion: + title: Rails version in Gemfile + description: + 'Set up the application with Gemfile pointing to a specific version + (options: dev, edge, master)' + type: string + enum: + - dev + - edge + - master + + steps: + - id: fetch-base + name: Fetch Base + action: fetch:rails + input: + url: ./template + values: + name: '{{ parameters.name }}' + owner: '{{ parameters.owner }}' + system: '{{ parameters.system }}' + railsArguments: '{{ json parameters.railsArguments }}' + + - name: Write Catalog information + action: catalog:write + input: + component: + apiVersion: 'backstage.io/v1alpha1' + kind: Component + metadata: + name: '{{ parameters.name }}' + annotations: + github.com/project-slug: '{{ projectSlug parameters.repoUrl }}' + spec: + type: service + lifecycle: production + owner: '{{ parameters.owner }}' + + - id: publish + if: '{{ not parameters.dryRun }}' + name: Publish + action: publish:github + input: + allowedHosts: ['github.com'] + description: 'This is {{ parameters.name }}' + repoUrl: '{{ parameters.repoUrl }}' + + - id: register + if: '{{ not parameters.dryRun }}' + name: Register + action: catalog:register + input: + repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + catalogInfoPath: '/catalog-info.yaml' + + - name: Results + if: '{{ parameters.dryRun }}' + action: debug:log + input: + listWorkspace: true + + output: + links: + - title: Repository + url: '{{ steps.publish.output.remoteUrl }}' + - title: Open in catalog + icon: 'catalog' + entityRef: '{{ steps.register.output.entityRef }}' +``` + +We have a [Docker image](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/Rails.dockerfile) you can use to build your own. diff --git a/plugins/scaffolder-backend/scripts/Rails.dockerfile b/plugins/scaffolder-backend-module-rails/Rails.dockerfile similarity index 100% rename from plugins/scaffolder-backend/scripts/Rails.dockerfile rename to plugins/scaffolder-backend-module-rails/Rails.dockerfile 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..79b123cdb6 --- /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", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.8.3", + "@backstage/plugin-scaffolder-backend": "^0.12.2", + "@backstage/config": "^0.1.5", + "@backstage/errors": "^0.1.1", + "@backstage/integration": "^0.5.6", + "command-exists": "^1.2.9", + "fs-extra": "^9.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.7.1", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/command-exists": "^1.2.0", + "@types/fs-extra": "^9.0.1", + "@types/mock-fs": "^4.13.0", + "jest-when": "^3.1.0", + "mock-fs": "^4.13.0" + }, + "files": [ + "dist" + ] +} 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/src/scaffolder/actions/builtin/fetch/rails.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts similarity index 86% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts rename to plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index ebc62e9277..cedc768f88 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -15,24 +15,31 @@ */ const mockRailsTemplater = { run: jest.fn() }; -jest.mock('./helpers'); -jest.mock('../../../stages/templater', () => { +jest.mock('@backstage/plugin-scaffolder-backend', () => ({ + ...jest.requireActual('@backstage/plugin-scaffolder-backend'), + fetchContents: jest.fn(), +})); +jest.mock('./railsNewRunner', () => { return { - Rails: jest.fn().mockImplementation(() => { + RailsNewRunner: jest.fn().mockImplementation(() => { return mockRailsTemplater; }), }; }); -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { + ContainerRunner, + getVoidLogger, + UrlReader, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import mock from 'mock-fs'; import os from 'os'; import { resolve as resolvePath } from 'path'; import { PassThrough } from 'stream'; -import { createFetchRailsAction } from './rails'; -import { fetchContents } from './helpers'; +import { createFetchRailsAction } from './index'; +import { fetchContents } from '@backstage/plugin-scaffolder-backend'; describe('fetch:rails', () => { const integrations = ScmIntegrations.fromConfig( @@ -68,10 +75,14 @@ describe('fetch:rails', () => { readTree: jest.fn(), search: jest.fn(), }; + const containerRunner: ContainerRunner = { + runContainer: jest.fn(), + }; const action = createFetchRailsAction({ integrations, reader: mockReader, + containerRunner, }); beforeEach(() => { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts similarity index 90% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts rename to plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 203db5004e..e2dc8fd95a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/rails.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -14,22 +14,25 @@ * limitations under the License. */ -import { DockerContainerRunner, UrlReader } from '@backstage/backend-common'; +import { ContainerRunner, UrlReader } from '@backstage/backend-common'; import { JsonObject } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; +import { + createTemplateAction, + fetchContents, +} from '@backstage/plugin-scaffolder-backend'; + import { resolve as resolvePath } from 'path'; -import { Rails, TemplaterValues } from '../../../stages/templater'; -import { createTemplateAction } from '../../createTemplateAction'; -import { fetchContents } from './helpers'; -import Docker from 'dockerode'; +import { RailsNewRunner } from './railsNewRunner'; export function createFetchRailsAction(options: { reader: UrlReader; integrations: ScmIntegrations; + containerRunner: ContainerRunner; }) { - const { reader, integrations } = options; + const { reader, integrations, containerRunner } = options; return createTemplateAction<{ url: string; @@ -39,7 +42,7 @@ export function createFetchRailsAction(options: { }>({ id: 'fetch:rails', description: - 'Downloads a template from the given URL into the workspace, and runs a rails generator on it.', + 'Downloads a template from the given URL into the workspace, and runs a rails new generator on it.', schema: { input: { type: 'object', @@ -152,12 +155,10 @@ export function createFetchRailsAction(options: { outputPath: workDir, }); - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); - const templateRunner = new Rails({ containerRunner }); + const templateRunner = new RailsNewRunner({ containerRunner }); const values = { - ...(ctx.input.values as TemplaterValues), + ...ctx.input.values, imageName: ctx.input.imageName, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts similarity index 95% rename from plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts rename to plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts index 370d9de380..a8c10625c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts @@ -15,7 +15,6 @@ */ import { railsArgumentResolver } from './railsArgumentResolver'; -import { TemplaterValues } from '../types'; describe('railsArgumentResolver', () => { describe('when provide the parameter', () => { @@ -33,7 +32,7 @@ describe('railsArgumentResolver', () => { 'should include the argument to execution %p -> %p', (passedArguments: object, expected: Array) => { // that step is to ensure the validation between the TemplaterValues and the resolver - const values: TemplaterValues = { + const values = { owner: 'r', storePath: '', railsArguments: passedArguments, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/railsArgumentResolver.ts rename to plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts similarity index 89% rename from plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts rename to plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts index 534899eec1..dc3f824236 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts @@ -16,7 +16,7 @@ const runCommand = jest.fn(); const commandExists = jest.fn(); -jest.mock('../helpers', () => ({ runCommand })); +jest.mock('@backstage/plugin-scaffolder-backend', () => ({ runCommand })); jest.mock('command-exists', () => commandExists); jest.mock('fs-extra'); @@ -26,7 +26,7 @@ import fs from 'fs-extra'; import path from 'path'; import { PassThrough } from 'stream'; -import { Rails } from './index'; +import { RailsNewRunner } from './railsNewRunner'; describe('Rails Templater', () => { const containerRunner: jest.Mocked = { @@ -39,6 +39,7 @@ describe('Rails Templater', () => { describe('when running on docker', () => { it('should run the correct bindings for the volumes', async () => { + const logStream = new PassThrough(); const values = { owner: 'angeliski', storePath: 'https://github.com/angeliski/rails-project', @@ -51,10 +52,11 @@ describe('Rails Templater', () => { .spyOn(fs, 'realpath') .mockImplementation(x => Promise.resolve(x.toString())); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await templater.run({ workspacePath: 'tempdir', values, + logStream, }); expect(containerRunner.runContainer).toHaveBeenCalledWith({ @@ -67,11 +69,12 @@ describe('Rails Templater', () => { [path.join('tempdir', 'intermediate')]: '/output', }, workingDir: '/input', - logStream: undefined, + logStream: logStream, }); }); it('should use the provided imageName', async () => { + const logStream = new PassThrough(); const values = { owner: 'angeliski', storePath: 'https://github.com/angeliski/rails-project', @@ -81,10 +84,11 @@ describe('Rails Templater', () => { jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await templater.run({ workspacePath: 'tempdir', values, + logStream, }); expect(containerRunner.runContainer).toHaveBeenCalledWith( @@ -106,7 +110,7 @@ describe('Rails Templater', () => { jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await templater.run({ workspacePath: 'tempdir', values, @@ -128,6 +132,7 @@ describe('Rails Templater', () => { }); it('update the template path to correct location', async () => { + const logStream = new PassThrough(); const values = { owner: 'angeliski', storePath: 'https://github.com/angeliski/rails-project', @@ -141,10 +146,11 @@ describe('Rails Templater', () => { .spyOn(fs, 'realpath') .mockImplementation(x => Promise.resolve(x.toString())); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await templater.run({ workspacePath: 'tempdir', values, + logStream, }); expect(containerRunner.runContainer).toHaveBeenCalledWith({ @@ -162,7 +168,7 @@ describe('Rails Templater', () => { [path.join('tempdir', 'intermediate')]: '/output', }, workingDir: '/input', - logStream: undefined, + logStream: logStream, }); }); }); @@ -181,7 +187,7 @@ describe('Rails Templater', () => { jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); commandExists.mockImplementationOnce(() => () => true); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await templater.run({ workspacePath: 'tempdir', values, @@ -211,7 +217,7 @@ describe('Rails Templater', () => { jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); commandExists.mockImplementationOnce(() => () => true); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await templater.run({ workspacePath: 'tempdir', values, @@ -239,7 +245,7 @@ describe('Rails Templater', () => { .spyOn(fs, 'readdir') .mockImplementationOnce(() => Promise.resolve([])); - const templater = new Rails({ containerRunner }); + const templater = new RailsNewRunner({ containerRunner }); await expect( templater.run({ workspacePath: 'tempdir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts similarity index 84% rename from plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts rename to plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts index 57ee26e106..3e18490587 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts @@ -17,12 +17,16 @@ import { ContainerRunner } from '@backstage/backend-common'; import fs from 'fs-extra'; import path from 'path'; -import { runCommand } from '../helpers'; +import { runCommand } from '@backstage/plugin-scaffolder-backend'; import commandExists from 'command-exists'; -import { TemplaterBase, TemplaterRunOptions } from '../types'; -import { railsArgumentResolver } from './railsArgumentResolver'; +import { + railsArgumentResolver, + RailsRunOptions, +} from './railsArgumentResolver'; +import { JsonObject } from '@backstage/config'; +import { Writable } from 'stream'; -export class Rails implements TemplaterBase { +export class RailsNewRunner { private readonly containerRunner: ContainerRunner; constructor({ containerRunner }: { containerRunner: ContainerRunner }) { @@ -33,7 +37,11 @@ export class Rails implements TemplaterBase { workspacePath, values, logStream, - }: TemplaterRunOptions): Promise { + }: { + workspacePath: string; + values: JsonObject; + logStream: Writable; + }): Promise { const intermediateDir = path.join(workspacePath, 'intermediate'); await fs.ensureDir(intermediateDir); const resultDir = path.join(workspacePath, 'result'); @@ -53,7 +61,7 @@ export class Rails implements TemplaterBase { if (commandExistsToRun) { const arrayExtraArguments = railsArgumentResolver( workspacePath, - railsArguments, + railsArguments as RailsRunOptions, ); await runCommand({ @@ -68,10 +76,10 @@ export class Rails implements TemplaterBase { } else { const arrayExtraArguments = railsArgumentResolver( '/input', - railsArguments, + railsArguments as RailsRunOptions, ); await this.containerRunner.runContainer({ - imageName: imageName, + imageName: imageName as string, command: baseCommand, args: [...baseArguments, `/output/${name}`, ...arrayExtraArguments], mountDirs, 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..e30542aca7 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; @@ -131,7 +131,7 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) - logger: Logger; + logger: Logger_2; // (undocumented) reader: UrlReader; // (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index a6db486594..a88413d611 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -62,8 +62,7 @@ "octokit-plugin-create-pull-request": "^3.9.3", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0", - "dockerode": "^3.2.1" + "yaml": "^1.10.0" }, "devDependencies": { "@backstage/cli": "^0.7.3", @@ -73,7 +72,6 @@ "@types/git-url-parse": "^9.0.0", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", - "@types/dockerode": "^3.2.1", "jest-when": "^3.1.0", "mock-fs": "^4.13.0", "msw": "^0.29.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index fa2fcbfcee..08d7bbf824 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -24,11 +24,7 @@ import { } from './catalog'; import { createDebugLogAction } from './debug'; -import { - createFetchCookiecutterAction, - createFetchPlainAction, - createFetchRailsAction, -} from './fetch'; +import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch'; import { createFilesystemDeleteAction, createFilesystemRenameAction, @@ -66,10 +62,6 @@ export const createBuiltinActions = (options: { integrations, containerRunner, }), - createFetchRailsAction({ - reader, - integrations, - }), createPublishGithubAction({ integrations, config, 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 9557a88a4c..82bc387f26 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts @@ -16,4 +16,4 @@ export { createFetchPlainAction } from './plain'; export { createFetchCookiecutterAction } from './cookiecutter'; -export { createFetchRailsAction } from './rails'; +export * from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts index 139ff2207e..4d80f86946 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts @@ -18,4 +18,3 @@ export * from './types'; export * from './helpers'; export * from './templaters'; export * from './cra'; -export * from './rails'; From 1cdc4ba0e2c36b1e1c0869cb6dbc9cfcb079d40f Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 24 Jun 2021 12:49:32 -0300 Subject: [PATCH 38/47] docs: added generated files Signed-off-by: Rogerio Angeliski --- packages/backend-common/api-report.md | 10 +++++----- packages/techdocs-common/api-report.md | 20 +++++++++---------- plugins/app-backend/api-report.md | 4 ++-- plugins/auth-backend/api-report.md | 6 +++--- .../api-report.md | 6 +++--- plugins/catalog-graphql/api-report.md | 4 ++-- plugins/code-coverage-backend/api-report.md | 4 ++-- plugins/graphql/api-report.md | 4 ++-- plugins/kafka-backend/api-report.md | 2 +- plugins/kubernetes-backend/api-report.md | 6 +++--- plugins/proxy-backend/api-report.md | 2 +- plugins/rollbar-backend/api-report.md | 6 +++--- plugins/search-backend-node/api-report.md | 8 ++++---- plugins/search-backend/api-report.md | 2 +- plugins/techdocs-backend/api-report.md | 2 +- plugins/todo-backend/api-report.md | 2 +- 16 files changed, 44 insertions(+), 44 deletions(-) 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/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-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index f1e7e22c35..bac733ef53 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) 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/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'; From c1a86b800a929c6deda00640535a240d07c7363c Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 24 Jun 2021 13:27:44 -0300 Subject: [PATCH 39/47] chore: remove frontend plugin command Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-backend-module-rails/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 79b123cdb6..b2765b04f5 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -14,7 +14,6 @@ "start": "backstage-cli plugin:serve", "lint": "backstage-cli lint", "test": "backstage-cli test", - "diff": "backstage-cli plugin:diff", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" From 93f8e8b423c633b4f7a535c2f5a1899bc1ae544b Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Tue, 29 Jun 2021 18:51:02 -0300 Subject: [PATCH 40/47] Revert "docs: added generated files" This reverts commit b2cefb34e0d4640cc5bb675d05d0ea906d74eec8. Signed-off-by: Rogerio Angeliski --- packages/backend-common/api-report.md | 10 +++++----- packages/techdocs-common/api-report.md | 20 +++++++++---------- plugins/app-backend/api-report.md | 4 ++-- plugins/auth-backend/api-report.md | 6 +++--- .../api-report.md | 6 +++--- plugins/catalog-graphql/api-report.md | 4 ++-- plugins/code-coverage-backend/api-report.md | 4 ++-- plugins/graphql/api-report.md | 4 ++-- plugins/kafka-backend/api-report.md | 2 +- plugins/kubernetes-backend/api-report.md | 6 +++--- plugins/proxy-backend/api-report.md | 2 +- plugins/rollbar-backend/api-report.md | 6 +++--- plugins/search-backend-node/api-report.md | 8 ++++---- plugins/search-backend/api-report.md | 2 +- plugins/techdocs-backend/api-report.md | 2 +- plugins/todo-backend/api-report.md | 2 +- 16 files changed, 44 insertions(+), 44 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 22eac06ed2..a21aaef4a6 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 as Logger_2 } from 'winston'; +import { Logger } 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_2; + logger?: Logger; logClientErrors?: boolean; }; @@ -185,7 +185,7 @@ export class Git { static fromAuth: ({ username, password, logger, }: { username?: string | undefined; password?: string | undefined; - logger?: Logger_2 | undefined; + logger?: Logger | undefined; }) => Git; // (undocumented) init({ dir, defaultBranch, }: { @@ -301,7 +301,7 @@ export type ReadTreeResponseFile = { }; // @public -export function requestLoggingHandler(logger?: Logger_2): RequestHandler; +export function requestLoggingHandler(logger?: Logger): 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_2): ServiceBuilder; + setLogger(logger: Logger): ServiceBuilder; enableCors(options: cors.CorsOptions): ServiceBuilder; setHttpsSettings(settings: HttpsSettings): ServiceBuilder; addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index d25f55bf3a..3fff030eee 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 as Logger_2 } from 'winston'; +import { Logger } 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_2) => Promise; +export const checkoutGitRepository: (repoUrl: string, config: Config, logger: Logger) => Promise; // @public (undocumented) export class CommonGitPreparer implements PreparerBase { - constructor(config: Config, logger: Logger_2); + constructor(config: Config, logger: Logger); // (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_2, reader: UrlReader); + constructor(config: Config, logger: Logger, 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_2; + logger: Logger; 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_2 | undefined; + logger?: Logger | 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_2) => Promise; +export const getLastCommitTimestamp: (repositoryLocation: string, logger: Logger) => 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_2; + logger?: Logger; 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_2; + logger: Logger; containerRunner: ContainerRunner; config: Config; }); @@ -170,7 +170,7 @@ export type TechDocsMetadata = { // @public (undocumented) export class UrlPreparer implements PreparerBase { - constructor(reader: UrlReader, logger: Logger_2); + constructor(reader: UrlReader, logger: Logger); // (undocumented) prepare(entity: Entity, options?: { etag?: string; diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index 93aba5596f..4f59102265 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 as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; @@ -18,7 +18,7 @@ export interface RouterOptions { config: Config; disableConfigInjection?: boolean; // (undocumented) - logger: Logger_2; + logger: Logger; staticFallbackHandler?: express.Handler; } diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 9f9a8d18ef..6ae878accf 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 as Logger_2 } from 'winston'; +import { Logger } 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_2; + logger: Logger; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; @@ -206,7 +206,7 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) providerFactories?: ProviderFactories; } diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index bac733ef53..f1e7e22c35 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 as Logger_2 } from 'winston'; +import { Logger } 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_2; + logger: Logger; groupTransformer?: GroupTransformer; }); // (undocumented) static fromConfig(config: Config, options: { - logger: Logger_2; + logger: Logger; groupTransformer?: GroupTransformer; }): MicrosoftGraphOrgReaderProcessor; // (undocumented) diff --git a/plugins/catalog-graphql/api-report.md b/plugins/catalog-graphql/api-report.md index 40e8a19b66..cbe325f4c5 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 as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public (undocumented) export function createModule(options: ModuleOptions): Promise; @@ -16,7 +16,7 @@ export interface ModuleOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index 186b76fb39..f8f1575d0b 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 as Logger_2 } from 'winston'; +import { Logger } 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_2; + logger: Logger; // (undocumented) urlReader: UrlReader; } diff --git a/plugins/graphql/api-report.md b/plugins/graphql/api-report.md index ef172afbd7..8109c94122 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 as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; @@ -16,7 +16,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index 6840bd7527..47754ad572 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 as Logger_2 } from 'winston'; +import { Logger } 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 a9389b7e62..fe43e8888a 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 as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public (undocumented) export interface ClusterDetails { @@ -68,7 +68,7 @@ export interface KubernetesServiceLocator { } // @public (undocumented) -export const makeRouter: (logger: Logger_2, kubernetesFanOutHandler: KubernetesFanOutHandler, clusterDetails: ClusterDetails[]) => express.Router; +export const makeRouter: (logger: Logger, kubernetesFanOutHandler: KubernetesFanOutHandler, clusterDetails: ClusterDetails[]) => express.Router; // @public (undocumented) export interface ObjectFetchParams { @@ -91,7 +91,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } // @public (undocumented) diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index 63469a2017..1b1eba3b00 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 as Logger_2 } from 'winston'; +import { Logger } 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 9fa525cfc3..fa8c67d4f0 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 as Logger_2 } from 'winston'; +import { Logger } 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_2); + constructor(accessToken: string, logger: Logger); // (undocumented) getActivatedCounts(projectName: string, options?: { environment: string; @@ -49,7 +49,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) rollbarApi?: RollbarApi; } diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index ccb7475573..03585b6b99 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 as Logger_2 } from 'winston'; +import { Logger } 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_2; + logger: Logger; }); // (undocumented) protected docStore: Record; // (undocumented) index(type: string, documents: IndexableDocument[]): void; // (undocumented) - protected logger: Logger_2; + protected logger: Logger; // (undocumented) protected lunrIndices: Record; // (undocumented) @@ -48,7 +48,7 @@ export class LunrSearchEngine implements SearchEngine { // @public export class Scheduler { constructor({ logger }: { - logger: Logger_2; + logger: Logger; }); 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 7eba677439..9e617b73ed 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 as Logger_2 } from 'winston'; +import { Logger } 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 947439533a..d433d428e0 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 as Logger_2 } from 'winston'; +import { Logger } 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 d203a52b36..8bcfe30aee 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 as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; From 944640535ac8d6fdf5d8d936b132c7aef571525b Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 1 Jul 2021 21:47:54 -0300 Subject: [PATCH 41/47] update with PR comments Signed-off-by: Rogerio Angeliski --- .../scaffolder-backend-module-rails/README.md | 26 ++++++++++++++++++- .../Rails.dockerfile | 6 ++--- .../package.json | 1 + .../src/actions/fetch/rails/index.test.ts | 6 ++--- .../fetch/rails/railsArgumentResolver.test.ts | 10 +++++-- .../fetch/rails/railsArgumentResolver.ts | 8 +++++- .../sample-templates/local-templates.yaml | 1 - .../scaffolder/actions/builtin/fetch/index.ts | 2 +- 8 files changed, 47 insertions(+), 13 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/README.md b/plugins/scaffolder-backend-module-rails/README.md index 3c0c9da53e..3da594dd75 100644 --- a/plugins/scaffolder-backend-module-rails/README.md +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -11,7 +11,10 @@ Here you can find all Rails related features to improve your scaffolder: 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 ``` @@ -222,4 +225,25 @@ spec: entityRef: '{{ steps.register.output.entityRef }}' ``` -We have a [Docker image](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/Rails.dockerfile) you can use to build your own. +### 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 index 3cec0e7668..0774fdc975 100644 --- a/plugins/scaffolder-backend-module-rails/Rails.dockerfile +++ b/plugins/scaffolder-backend-module-rails/Rails.dockerfile @@ -1,9 +1,7 @@ FROM ruby:3.0 RUN apt-get update -qq && \ - apt-get install -y \ - nodejs \ - postgresql-client \ - git + 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/package.json b/plugins/scaffolder-backend-module-rails/package.json index b2765b04f5..b513100f16 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -4,6 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", 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 index cedc768f88..6b98dd86c6 100644 --- 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 @@ -34,7 +34,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import mock from 'mock-fs'; +import mockFs from 'mock-fs'; import os from 'os'; import { resolve as resolvePath } from 'path'; import { PassThrough } from 'stream'; @@ -86,12 +86,12 @@ describe('fetch:rails', () => { }); beforeEach(() => { - mock({ [`${mockContext.workspacePath}/result`]: {} }); + mockFs({ [`${mockContext.workspacePath}/result`]: {} }); jest.restoreAllMocks(); }); afterEach(() => { - mock.restore(); + mockFs.restore(); }); it('should call fetchContents with the correct values', async () => { 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 index a8c10625c2..99a113f4f7 100644 --- 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 @@ -15,9 +15,12 @@ */ 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']], @@ -27,7 +30,10 @@ describe('railsArgumentResolver', () => { [{ webpacker: 'vue' }, ['--webpack', 'vue']], [{ database: 'postgresql' }, ['--database', 'postgresql']], [{ railsVersion: 'dev' }, ['--dev']], - [{ template: './rails.rb' }, ['--template', '/tmp/rails.rb']], + [ + { template: `.${separatorPath}rails.rb` }, + ['--template', `${root}${separatorPath}rails.rb`], + ], ])( 'should include the argument to execution %p -> %p', (passedArguments: object, expected: Array) => { @@ -40,7 +46,7 @@ describe('railsArgumentResolver', () => { const { railsArguments } = values; - const argumentsToRun = railsArgumentResolver('/tmp', railsArguments); + 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 index 5ea2f70895..b1ddadc6d9 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { sep as separatorPath } from 'path'; enum Webpacker { react = 'react', @@ -99,7 +100,12 @@ export const railsArgumentResolver = ( if (options?.template) { argumentsToRun.push('--template'); - argumentsToRun.push(options.template.replace('./', `${projectRoot}/`)); + argumentsToRun.push( + options.template.replace( + `.${separatorPath}`, + `${projectRoot}${separatorPath}`, + ), + ); } return argumentsToRun; diff --git a/plugins/scaffolder-backend/sample-templates/local-templates.yaml b/plugins/scaffolder-backend/sample-templates/local-templates.yaml index 59a55b3cd9..3b3acbae7e 100644 --- a/plugins/scaffolder-backend/sample-templates/local-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/local-templates.yaml @@ -10,5 +10,4 @@ spec: - ./create-react-app/template.yaml - ./springboot-grpc-template/template.yaml - ./v1beta2-demo/template.yaml - - ./rails-demo/template.yaml - ./pull-request/template.yaml 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 82bc387f26..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,4 +16,4 @@ export { createFetchPlainAction } from './plain'; export { createFetchCookiecutterAction } from './cookiecutter'; -export * from './helpers'; +export { fetchContents } from './helpers'; From 7a74046f1673c626066516f60cd60d900820161c Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Mon, 5 Jul 2021 21:21:00 -0300 Subject: [PATCH 42/47] docs: added generated files Signed-off-by: Rogerio Angeliski --- packages/backend-common/api-report.md | 10 +++++----- packages/techdocs-common/api-report.md | 20 +++++++++---------- plugins/app-backend/api-report.md | 4 ++-- plugins/auth-backend/api-report.md | 6 +++--- .../catalog-backend-module-ldap/api-report.md | 10 +++++----- .../api-report.md | 8 ++++---- plugins/catalog-graphql/api-report.md | 4 ++-- plugins/code-coverage-backend/api-report.md | 4 ++-- plugins/graphql/api-report.md | 4 ++-- plugins/kafka-backend/api-report.md | 2 +- plugins/kubernetes-backend/api-report.md | 6 +++--- plugins/proxy-backend/api-report.md | 2 +- plugins/rollbar-backend/api-report.md | 6 +++--- plugins/search-backend-node/api-report.md | 8 ++++---- plugins/search-backend/api-report.md | 2 +- plugins/techdocs-backend/api-report.md | 2 +- plugins/todo-backend/api-report.md | 2 +- 17 files changed, 50 insertions(+), 50 deletions(-) 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/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/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'; From 02b962394b19a73c829640c86f1e6cd54841b1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 7 Jul 2021 13:27:39 +0200 Subject: [PATCH 43/47] Added a `context` parameter to validator functions, letting them have access to the API holder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/great-bears-work.md | 10 ++++++ plugins/scaffolder/api-report.md | 13 ++++++++ .../components/TemplatePage/TemplatePage.tsx | 33 ++++++++++++++----- .../fields/RepoUrlPicker/validation.test.ts | 3 +- .../fields/RepoUrlPicker/validation.ts | 1 + plugins/scaffolder/src/extensions/index.tsx | 4 +-- plugins/scaffolder/src/extensions/types.ts | 11 ++++++- plugins/scaffolder/src/index.ts | 6 ++-- 8 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 .changeset/great-bears-work.md diff --git a/.changeset/great-bears-work.md b/.changeset/great-bears-work.md new file mode 100644 index 0000000000..572cf1538d --- /dev/null +++ b/.changeset/great-bears-work.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added a `context` parameter to validator functions, letting them have access to +the API holder. + +If you have implemented custom validators and use `createScaffolderFieldExtension`, +your `validation` function can now optionally accept a third parameter, +`context: { apiHolder: ApiHolder }`. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 5da1869fc2..7982ad997e 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -4,6 +4,7 @@ ```ts +import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; @@ -24,9 +25,21 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; // @public (undocumented) export function createScaffolderFieldExtension(options: FieldExtensionOptions): Extension<() => null>; +// @public (undocumented) +export type CustomFieldValidator = ((data: T, field: FieldValidation) => void) | ((data: T, field: FieldValidation, context: { + apiHolder: ApiHolder; +}) => void); + // @public (undocumented) export const EntityPickerFieldExtension: () => null; +// @public (undocumented) +export type FieldExtensionOptions = { + name: string; + component: (props: FieldProps) => JSX.Element | null; + validation?: CustomFieldValidator; +}; + // @public (undocumented) export const OwnerPickerFieldExtension: () => null; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index b6cab5b620..e72255f129 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -15,13 +15,13 @@ */ import { JsonObject, JsonValue } from '@backstage/config'; import { LinearProgress } from '@material-ui/core'; -import { FieldValidation, FormValidation, IChangeEvent } from '@rjsf/core'; +import { FormValidation, IChangeEvent } from '@rjsf/core'; import React, { useCallback, useState } from 'react'; import { generatePath, Navigate, useNavigate } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { scaffolderApiRef } from '../../api'; -import { FieldExtensionOptions } from '../../extensions'; +import { CustomFieldValidator, FieldExtensionOptions } from '../../extensions'; import { rootRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; @@ -32,7 +32,13 @@ import { Lifecycle, Page, } from '@backstage/core-components'; -import { errorApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + ApiHolder, + errorApiRef, + useApi, + useApiHolder, + useRouteRef, +} from '@backstage/core-plugin-api'; const useTemplateParameterSchema = (templateName: string) => { const scaffolderApi = useApi(scaffolderApiRef); @@ -54,10 +60,10 @@ function isObject(obj: unknown): obj is JsonObject { export const createValidator = ( rootSchema: JsonObject, - validators: Record< - string, - undefined | ((value: JsonValue, validation: FieldValidation) => void) - >, + validators: Record>, + context: { + apiHolder: ApiHolder; + }, ) => { function validate( schema: JsonObject, @@ -86,7 +92,11 @@ export const createValidator = ( const fieldName = isObject(propSchema) && (propSchema['ui:field'] as string); if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!(propData as JsonValue, propValidation); + validators[fieldName]!( + propData as JsonValue, + propValidation, + context, + ); } } } @@ -103,6 +113,7 @@ export const TemplatePage = ({ }: { customFieldExtensions?: FieldExtensionOptions[]; }) => { + const apiHolder = useApiHolder(); const errorApi = useApi(errorApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); @@ -171,7 +182,11 @@ export const TemplatePage = ({ steps={schema.steps.map(step => { return { ...step, - validate: createValidator(step.schema, customFieldValidators), + validate: createValidator( + step.schema, + customFieldValidators, + { apiHolder }, + ), }; })} /> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts index f0df198507..1ea66e7df9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { repoPickerValidation } from './validation'; import { FieldValidation } from '@rjsf/core'; @@ -22,7 +23,7 @@ describe('RepoPicker Validation', () => { addError: jest.fn(), } as unknown) as FieldValidation); - it('validaties when no repo', () => { + it('validates when no repo', () => { const mockFieldValidation = fieldValidator(); repoPickerValidation('github.com?owner=a', mockFieldValidation); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 827afa3cab..760b9f7890 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { FieldValidation } from '@rjsf/core'; export const repoPickerValidation = ( diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index f53052cb67..131cd105da 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { FieldExtensionOptions } from './types'; +import { CustomFieldValidator, FieldExtensionOptions } from './types'; import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; @@ -46,6 +46,6 @@ attachComponentData( true, ); -export type { FieldExtensionOptions }; +export type { CustomFieldValidator, FieldExtensionOptions }; export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default'; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 8f1c155f28..644897f7f4 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -13,10 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApiHolder } from '@backstage/core-plugin-api'; import { FieldValidation, FieldProps } from '@rjsf/core'; +export type CustomFieldValidator = + | ((data: T, field: FieldValidation) => void) + | (( + data: T, + field: FieldValidation, + context: { apiHolder: ApiHolder }, + ) => void); + export type FieldExtensionOptions = { name: string; component: (props: FieldProps) => JSX.Element | null; - validation?: (data: T, field: FieldValidation) => void; + validation?: CustomFieldValidator; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 6025abc067..a034f5f574 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -16,9 +16,11 @@ export { scaffolderApiRef, ScaffolderClient } from './api'; export type { ScaffolderApi } from './api'; -export { - createScaffolderFieldExtension, +export { createScaffolderFieldExtension } from './extensions'; +export type { ScaffolderFieldExtensions, + CustomFieldValidator, + FieldExtensionOptions, } from './extensions'; export { EntityPickerFieldExtension, From 32a7c5d975fb62de5426135e9189885247ec6dd7 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 7 Jul 2021 09:33:39 -0300 Subject: [PATCH 44/47] update rails plugin docs Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-backend-module-rails/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/README.md b/plugins/scaffolder-backend-module-rails/README.md index 3da594dd75..4ac52be941 100644 --- a/plugins/scaffolder-backend-module-rails/README.md +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -32,9 +32,7 @@ const actions = [ ]; return await createRouter({ - preparers, - templaters, - publishers, + containerRunner, logger, config, database, From 75b3d3d9815a8068862c0c0dad89f7cfcd37da92 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 7 Jul 2021 09:34:04 -0300 Subject: [PATCH 45/47] fix runCommand export Signed-off-by: Rogerio Angeliski --- .../src/scaffolder/actions/builtin/index.ts | 1 + .../src/scaffolder/stages/templater/index.ts | 20 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts 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/scaffolder-backend/src/scaffolder/stages/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts deleted file mode 100644 index 4d80f86946..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from './cookiecutter'; -export * from './types'; -export * from './helpers'; -export * from './templaters'; -export * from './cra'; From 265e0851f4a79685ff06dec2b7becc7ff89dad63 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 7 Jul 2021 09:41:25 -0300 Subject: [PATCH 46/47] docs: added generated files Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-backend/api-report.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index e30542aca7..e8e8697291 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -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) @@ -138,6 +147,9 @@ export interface RouterOptions { taskWorkers?: number; } +// @public (undocumented) +export const runCommand: ({ command, args, logStream, }: RunCommandOptions) => Promise; + // @public (undocumented) export type TemplateAction = { id: string; From 1bcf50971252f63f7de8c8f80248f2cea8b95b8c Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 7 Jul 2021 09:45:58 -0300 Subject: [PATCH 47/47] remove wrong dependency Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-backend/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index a88413d611..7173f865c5 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -40,8 +40,6 @@ "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", "azure-devops-node-api": "^10.2.2", - "@types/git-url-parse": "^9.0.0", - "azure-devops-node-api": "^10.1.1", "command-exists": "^1.2.9", "compression": "^1.7.4", "cors": "^2.8.5",