From 84097c7da02dd9e92405f818b03d6e81e219dddd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 21 Sep 2024 12:42:25 +0200 Subject: [PATCH] cli: remove experimental onboard command Signed-off-by: Patrik Oldsberg --- .changeset/sweet-chicken-smash.md | 2 +- packages/cli/src/commands/index.ts | 8 - packages/cli/src/commands/onboard/README.md | 22 --- .../cli/src/commands/onboard/auth/index.ts | 63 ------- .../cli/src/commands/onboard/auth/patch.ts | 43 ----- .../patches/01-github.Add-SignInPage.patch | 25 --- .../patches/02-gitlab.Add-SignInPage.patch | 25 --- .../03-gitlab.Change-signIn-resolver.patch | 5 - .../commands/onboard/auth/providers/github.ts | 147 ---------------- .../commands/onboard/auth/providers/gitlab.ts | 115 ------------ packages/cli/src/commands/onboard/command.ts | 83 --------- packages/cli/src/commands/onboard/config.ts | 45 ----- .../commands/onboard/discovery/Discovery.ts | 71 -------- .../analyzers/BasicRepositoryAnalyzer.ts | 56 ------ .../analyzers/DefaultAnalysisOutputs.ts | 29 ---- .../analyzers/PackageJsonAnalyzer.ts | 128 -------------- .../onboard/discovery/analyzers/types.ts | 37 ---- .../src/commands/onboard/discovery/index.ts | 141 --------------- .../github/GithubDiscoveryProvider.ts | 163 ------------------ .../discovery/providers/github/GithubFile.ts | 35 ---- .../providers/github/GithubRepository.ts | 84 --------- .../gitlab/GitlabDiscoveryProvider.ts | 106 ------------ .../discovery/providers/gitlab/GitlabFile.ts | 38 ---- .../providers/gitlab/GitlabProject.ts | 89 ---------- .../onboard/discovery/providers/types.ts | 53 ------ packages/cli/src/commands/onboard/files.ts | 36 ---- packages/cli/src/commands/onboard/index.ts | 17 -- .../commands/onboard/integrations/github.ts | 136 --------------- .../commands/onboard/integrations/index.ts | 75 -------- 29 files changed, 1 insertion(+), 1876 deletions(-) delete mode 100644 packages/cli/src/commands/onboard/README.md delete mode 100644 packages/cli/src/commands/onboard/auth/index.ts delete mode 100644 packages/cli/src/commands/onboard/auth/patch.ts delete mode 100644 packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch delete mode 100644 packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch delete mode 100644 packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch delete mode 100644 packages/cli/src/commands/onboard/auth/providers/github.ts delete mode 100644 packages/cli/src/commands/onboard/auth/providers/gitlab.ts delete mode 100644 packages/cli/src/commands/onboard/command.ts delete mode 100644 packages/cli/src/commands/onboard/config.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/Discovery.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/analyzers/BasicRepositoryAnalyzer.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/analyzers/DefaultAnalysisOutputs.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/analyzers/PackageJsonAnalyzer.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/analyzers/types.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/index.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/github/GithubFile.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/github/GithubRepository.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabDiscoveryProvider.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabFile.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabProject.ts delete mode 100644 packages/cli/src/commands/onboard/discovery/providers/types.ts delete mode 100644 packages/cli/src/commands/onboard/files.ts delete mode 100644 packages/cli/src/commands/onboard/index.ts delete mode 100644 packages/cli/src/commands/onboard/integrations/github.ts delete mode 100644 packages/cli/src/commands/onboard/integrations/index.ts diff --git a/.changeset/sweet-chicken-smash.md b/.changeset/sweet-chicken-smash.md index a76c3d3fcc..9aa4c60803 100644 --- a/.changeset/sweet-chicken-smash.md +++ b/.changeset/sweet-chicken-smash.md @@ -11,4 +11,4 @@ - `versions:check`: Use `yarn dedupe` or `yarn-deduplicate` instead - `clean`: Use `backstage-cli package clean` instead -In addition, the experimental `install` command has been removed since it has not received any updates since its introduction and we're expecting usage to be low. If you where relying on this command, please let us know by opening an issue towards the main Backstage repository. +In addition, the experimental `install` and `onboard` commands have been removed since they have not received any updates since their introduction and we're expecting usage to be low. If you where relying on these commands, please let us know by opening an issue towards the main Backstage repository. diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 12f8551abc..50ac934897 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -25,13 +25,6 @@ const configOption = [ Array(), ] as const; -export function registerOnboardCommand(program: Command) { - program - .command('onboard', { hidden: true }) - .description('Get help setting up your Backstage App.') - .action(lazy(() => import('./onboard').then(m => m.command))); -} - export function registerRepoCommand(program: Command) { const command = program .command('repo [command]') @@ -328,7 +321,6 @@ export function registerCommands(program: Command) { registerRepoCommand(program); registerScriptCommand(program); registerMigrateCommand(program); - registerOnboardCommand(program); program .command('versions:bump') diff --git a/packages/cli/src/commands/onboard/README.md b/packages/cli/src/commands/onboard/README.md deleted file mode 100644 index 0fb4d22666..0000000000 --- a/packages/cli/src/commands/onboard/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# onboard - -## Local dev - -# Create a new app - -```bash -cd .. -npx @backstage/create-app -cd new-backstage-app - -# Run the modded cli in the new app - -../RELATIVE_PATH_TO_PROJECT/backstage/packages/cli/bin/backstage-cli onboard -? Do you want to set up Authentication for this project? (Y/n) -... - -# Try the app with the new changes - -yarn dev - -``` diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts deleted file mode 100644 index 0f4537b110..0000000000 --- a/packages/cli/src/commands/onboard/auth/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2023 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 inquirer from 'inquirer'; -import { Task } from '../../../lib/tasks'; -import { github, Answers as GitHubAnswers } from './providers/github'; -import { gitlab, Answers as GitLabAnswers } from './providers/gitlab'; - -export { type GitHubAnswers, type GitLabAnswers }; - -export async function auth(): Promise<{ - provider: string; - answers: GitHubAnswers | GitLabAnswers; -}> { - const answers = await inquirer.prompt<{ - provider: string; - }>([ - { - type: 'list', - name: 'provider', - message: 'Please select an authentication provider:', - choices: ['GitHub', 'GitLab'], - }, - ]); - - const { provider } = answers; - - let providerAnswers; - switch (provider) { - case 'GitHub': { - providerAnswers = await github(); - break; - } - case 'GitLab': { - providerAnswers = await gitlab(); - break; - } - default: - throw new Error(`Provider ${provider} not implemented yet.`); - } - - Task.log(); - Task.log(`Done setting up ${provider} Authentication!`); - Task.log(); - - return { - provider, - answers: providerAnswers, - }; -} diff --git a/packages/cli/src/commands/onboard/auth/patch.ts b/packages/cli/src/commands/onboard/auth/patch.ts deleted file mode 100644 index d004eeb157..0000000000 --- a/packages/cli/src/commands/onboard/auth/patch.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023 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 * as fs from 'fs-extra'; -import * as path from 'path'; -import * as differ from 'diff'; -import { PATCH_FOLDER } from '../files'; -import { findPaths } from '@backstage/cli-common'; - -/* eslint-disable-next-line no-restricted-syntax */ -const { targetRoot } = findPaths(__dirname); - -export const patch = async (patchFile: string) => { - const patchContent = await fs.readFile( - path.join(PATCH_FOLDER, patchFile), - 'utf8', - ); - const targetName = patchContent.split('\n')[0].replace('--- a', ''); - const targetFile = path.join(targetRoot, targetName); - const oldContent = await fs.readFile(targetFile, 'utf8'); - const newContent = differ.applyPatch(oldContent, patchContent); - if (!newContent) { - throw new Error( - `Patch ${patchFile} was not applied correctly. - Did you change ${targetName} manually before running this command?`, - ); - } - - return await fs.writeFile(targetFile, newContent, 'utf8'); -}; diff --git a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch deleted file mode 100644 index db9f5228e8..0000000000 --- a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch +++ /dev/null @@ -1,25 +0,0 @@ ---- a/packages/app/src/App.tsx -+++ b/packages/app/src/App.tsx -@@ -30 +30,5 @@ import { Root } from './components/Root'; --import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; -+import { -+ AlertDisplay, -+ OAuthRequestDialog, -+ SignInPage, -+} from '@backstage/core-components'; -@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/ -+import { githubAuthApiRef } from '@backstage/core-plugin-api'; -@@ -38,0 +44,14 @@ const app = createApp({ -+ components: { -+ SignInPage: props => ( -+ -+ ), -+ }, \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch deleted file mode 100644 index da849220b8..0000000000 --- a/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch +++ /dev/null @@ -1,25 +0,0 @@ ---- a/packages/app/src/App.tsx -+++ b/packages/app/src/App.tsx -@@ -30 +30,5 @@ import { Root } from './components/Root'; --import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; -+import { -+ AlertDisplay, -+ OAuthRequestDialog, -+ SignInPage, -+} from '@backstage/core-components'; -@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/ -+import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; -@@ -38,0 +44,14 @@ const app = createApp({ -+ components: { -+ SignInPage: props => ( -+ -+ ), -+ }, \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch deleted file mode 100644 index 7a69669674..0000000000 --- a/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch +++ /dev/null @@ -1,5 +0,0 @@ ---- a/packages/backend/src/plugins/auth.ts -+++ b/packages/backend/src/plugins/auth.ts -@@ -38 +38 @@ export default async function createPlugin( -- github: providers.github.create({ -+ gitlab: providers.gitlab.create({ \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/providers/github.ts b/packages/cli/src/commands/onboard/auth/providers/github.ts deleted file mode 100644 index 36c6b02038..0000000000 --- a/packages/cli/src/commands/onboard/auth/providers/github.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2023 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 { OAuthApp } from '@octokit/oauth-app'; -import chalk from 'chalk'; -import * as fs from 'fs-extra'; -import inquirer from 'inquirer'; -import { Task } from '../../../../lib/tasks'; -import { updateConfigFile } from '../../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files'; -import { patch } from '../patch'; - -export type Answers = { - clientSecret: string; - clientId: string; - hasEnterprise: boolean; - enterpriseInstanceUrl?: string; -}; - -const validateCredentials = async (clientId: string, clientSecret: string) => { - try { - const app = new OAuthApp({ - clientId, - clientSecret, - }); - await app.createToken({ - code: '%NOT-VALID-CODE%', - }); - } catch (error) { - // @octokit/request returns a error.response object when a request is rejected. - // We can check it to see what kind of error we received. - - // If error.response is successful we can double-check that the error itself was due to the bad code. - // If that's the case then we can assume that the client id and secret exists as we otherwise would - // have gotten a 400/404. - if ( - error.response.status !== 200 && - error.response.data.error !== 'bad_verification_code' - ) { - throw new Error(`Validating GitHub Credentials failed.`); - } - } -}; - -const getConfig = (answers: Answers) => { - const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = - answers; - - return { - auth: { - providers: { - github: { - development: { - clientId, - clientSecret, - ...(hasEnterprise && { - enterpriseInstanceUrl, - }), - }, - }, - }, - }, - }; -}; - -export const github = async (): Promise => { - Task.log(` - To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( - 'https://github.com/settings/developers', - )} - The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend. - - Settings for local development: - ${chalk.cyan(` - Homepage URL: http://localhost:3000 - Authorization callback URL: http://localhost:7007/api/auth/github/handler/frame`)} - - You can find the full documentation page here: ${chalk.blue( - 'https://backstage.io/docs/auth/github/provider', - )} - `); - - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'clientId', - message: 'What is your Client Id?', - validate: (input: string) => (input.length ? true : false), - }, - { - type: 'input', - name: 'clientSecret', - message: 'What is your Client Secret?', - validate: (input: string) => (input.length ? true : false), - }, - { - type: 'confirm', - name: 'hasEnterprise', - message: 'Are you using GitHub Enterprise?', - }, - { - type: 'input', - name: 'enterpriseInstanceUrl', - message: 'What is your URL for GitHub Enterprise?', - when: ({ hasEnterprise }) => hasEnterprise, - validate: (input: string) => Boolean(new URL(input)), - }, - ]); - - const { clientId, clientSecret } = answers; - const config = getConfig(answers); - - Task.log('Setting up GitHub Authentication for you...'); - - await Task.forItem( - 'Validating', - 'credentials', - async () => await validateCredentials(clientId, clientSecret), - ); - await Task.forItem( - 'Updating', - APP_CONFIG_FILE, - async () => await updateConfigFile(APP_CONFIG_FILE, config), - ); - - const patches = await fs.readdir(PATCH_FOLDER); - for (const patchFile of patches.filter(p => p.includes('github'))) { - await Task.forItem('Patching', patchFile, async () => { - await patch(patchFile); - }); - } - - return answers; -}; diff --git a/packages/cli/src/commands/onboard/auth/providers/gitlab.ts b/packages/cli/src/commands/onboard/auth/providers/gitlab.ts deleted file mode 100644 index bc5491111c..0000000000 --- a/packages/cli/src/commands/onboard/auth/providers/gitlab.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2023 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 chalk from 'chalk'; -import * as fs from 'fs-extra'; -import inquirer from 'inquirer'; -import { Task } from '../../../../lib/tasks'; -import { updateConfigFile } from '../../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files'; -import { patch } from '../patch'; - -const getConfig = (answers: Answers) => { - const { clientId, clientSecret, hasAudience, audience } = answers; - - return { - auth: { - providers: { - gitlab: { - development: { - clientId, - clientSecret, - ...(hasAudience && { - audience, - }), - }, - }, - }, - }, - }; -}; - -export type Answers = { - clientSecret: string; - clientId: string; - hasAudience: boolean; - audience?: string; -}; - -export const gitlab = async (): Promise => { - Task.log(` - To add GitLab authentication, you must create an Application from the GitLab Settings: ${chalk.blue( - 'https://gitlab.com/-/profile/applications', - )} - The Redirect URI should point to your Backstage backend auth handler. - - Settings for local development: - ${chalk.cyan(` - Name: Backstage (or your custom app name) - Redirect URI: http://localhost:7007/api/auth/gitlab/handler/frame - Scopes: read_api and read_user`)} - - You can find the full documentation page here: ${chalk.blue( - 'https://backstage.io/docs/auth/gitlab/provider', - )} - `); - - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'clientId', - message: 'What is your Application Id?', - validate: (input: string) => (input.length ? true : false), - }, - { - type: 'input', - name: 'clientSecret', - message: 'What is your Application Secret?', - validate: (input: string) => (input.length ? true : false), - }, - { - type: 'confirm', - name: 'hasAudience', - message: 'Do you have a self-hosted instance of GitLab?', - }, - { - type: 'input', - name: 'audience', - message: 'What is the URL for your GitLab instance?', - when: ({ hasAudience }) => hasAudience, - validate: (input: string) => Boolean(new URL(input)), - }, - ]); - - const config = getConfig(answers); - - Task.log('Setting up GitLab Authentication for you...'); - - await Task.forItem( - 'Updating', - APP_CONFIG_FILE, - async () => await updateConfigFile(APP_CONFIG_FILE, config), - ); - - const patches = await fs.readdir(PATCH_FOLDER); - for (const patchFile of patches.filter(p => p.includes('gitlab'))) { - await Task.forItem('Patching', patchFile, async () => { - await patch(patchFile); - }); - } - - return answers; -}; diff --git a/packages/cli/src/commands/onboard/command.ts b/packages/cli/src/commands/onboard/command.ts deleted file mode 100644 index f68685f3e6..0000000000 --- a/packages/cli/src/commands/onboard/command.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 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 chalk from 'chalk'; -import inquirer from 'inquirer'; -import { Task } from '../../lib/tasks'; -import { auth } from './auth'; -import { integrations } from './integrations'; -import { discover } from './discovery'; - -export async function command(): Promise { - const answers = await inquirer.prompt<{ - shouldSetupAuth: boolean; - shouldSetupScaffolder: boolean; - shouldDiscoverEntities: boolean; - }>([ - { - type: 'confirm', - name: 'shouldSetupAuth', - message: 'Do you want to set up Authentication for this project?', - default: true, - }, - { - type: 'confirm', - name: 'shouldSetupScaffolder', - message: 'Do you want to use Software Templates in this project?', - default: true, - }, - { - type: 'confirm', - name: 'shouldDiscoverEntities', - message: - 'Do you want to discover entities and add them to the Software Catalog?', - default: true, - }, - ]); - - const { shouldSetupAuth, shouldSetupScaffolder, shouldDiscoverEntities } = - answers; - - if (!shouldSetupAuth && !shouldSetupScaffolder && !shouldDiscoverEntities) { - Task.log( - chalk.yellow( - 'If you change your mind, feel free to re-run this command.', - ), - ); - return; - } - - let providerInfo; - if (shouldSetupAuth) { - providerInfo = await auth(); - } - - if (shouldSetupScaffolder) { - await integrations(providerInfo); - } - - if (shouldDiscoverEntities) { - await discover(providerInfo); - } - - Task.log(); - Task.log( - `You can now start your app with ${chalk.inverse( - chalk.italic('yarn dev'), - )}`, - ); - Task.log(); -} diff --git a/packages/cli/src/commands/onboard/config.ts b/packages/cli/src/commands/onboard/config.ts deleted file mode 100644 index cce3119fcc..0000000000 --- a/packages/cli/src/commands/onboard/config.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 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 * as fs from 'fs-extra'; -import yaml from 'yaml'; - -const readYaml = async (file: string) => { - return yaml.parse(await fs.readFile(file, 'utf8')); -}; - -export const updateConfigFile = async (file: string, config: T) => { - const staticContent = - '# Backstage override configuration for your local development environment \n'; - - const content = fs.existsSync(file) - ? yaml.stringify( - { ...(await readYaml(file)), ...config }, - { - indent: 2, - }, - ) - : staticContent.concat( - yaml.stringify( - { ...config }, - { - indent: 2, - }, - ), - ); - - return await fs.writeFile(file, content, 'utf8'); -}; diff --git a/packages/cli/src/commands/onboard/discovery/Discovery.ts b/packages/cli/src/commands/onboard/discovery/Discovery.ts deleted file mode 100644 index b0db4255ad..0000000000 --- a/packages/cli/src/commands/onboard/discovery/Discovery.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2023 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 chalk from 'chalk'; -import { Entity } from '@backstage/catalog-model'; -import { Analyzer } from './analyzers/types'; -import { Provider } from './providers/types'; -import { DefaultAnalysisOutputs } from './analyzers/DefaultAnalysisOutputs'; -import { Task } from '../../../lib/tasks'; - -export class Discovery { - readonly #providers: Provider[] = []; - readonly #analyzers: Analyzer[] = []; - - addProvider(provider: Provider) { - this.#providers.push(provider); - } - - addAnalyzer(analyzer: Analyzer) { - this.#analyzers.push(analyzer); - } - - async run(url: string): Promise<{ entities: Entity[] }> { - Task.log(`Running discovery for ${chalk.cyan(url)}`); - const result: Entity[] = []; - - for (const provider of this.#providers) { - const repositories = await provider.discover(url); - if (repositories && repositories.length) { - Task.log( - `Discovered ${chalk.cyan( - repositories.length, - )} repositories for ${chalk.cyan(provider.name())}`, - ); - - for (const repository of repositories) { - await Task.forItem('Analyzing', repository.name, async () => { - const output = new DefaultAnalysisOutputs(); - for (const analyzer of this.#analyzers) { - await analyzer.analyzeRepository({ repository, output }); - } - - output - .list() - .filter(entry => entry.type === 'entity') - .forEach(({ entity }) => result.push(entity)); - }); - } - - Task.log(`Produced ${chalk.cyan(result.length || 'no')} entities`); - } - } - - return { - entities: result, - }; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/analyzers/BasicRepositoryAnalyzer.ts b/packages/cli/src/commands/onboard/discovery/analyzers/BasicRepositoryAnalyzer.ts deleted file mode 100644 index 82c8c2cdae..0000000000 --- a/packages/cli/src/commands/onboard/discovery/analyzers/BasicRepositoryAnalyzer.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 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 { ComponentEntity } from '@backstage/catalog-model'; -import { AnalysisOutputs, Analyzer } from './types'; -import { Repository } from '../providers/types'; - -/** - * Naive analyzer that produces a single entity that represents the repository - * as a whole. - */ -export class BasicRepositoryAnalyzer implements Analyzer { - name(): string { - return BasicRepositoryAnalyzer.name; - } - - async analyzeRepository(options: { - repository: Repository; - output: AnalysisOutputs; - }): Promise { - const entity: ComponentEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: options.repository.name, - ...(options.repository.description - ? { description: options.repository.description } - : {}), - }, - spec: { - type: 'service', - lifecycle: 'production', - owner: 'user:guest', - }, - }; - - options.output.produce({ - type: 'entity', - path: '/', - entity: entity, - }); - } -} diff --git a/packages/cli/src/commands/onboard/discovery/analyzers/DefaultAnalysisOutputs.ts b/packages/cli/src/commands/onboard/discovery/analyzers/DefaultAnalysisOutputs.ts deleted file mode 100644 index a370b8f166..0000000000 --- a/packages/cli/src/commands/onboard/discovery/analyzers/DefaultAnalysisOutputs.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2023 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 { AnalysisOutput, AnalysisOutputs } from './types'; - -export class DefaultAnalysisOutputs implements AnalysisOutputs { - readonly #outputs = new Map(); - - produce(output: AnalysisOutput) { - this.#outputs.set(output.entity.metadata.name, output); - } - - list() { - return Array.from(this.#outputs).map(([_, output]) => output); - } -} diff --git a/packages/cli/src/commands/onboard/discovery/analyzers/PackageJsonAnalyzer.ts b/packages/cli/src/commands/onboard/discovery/analyzers/PackageJsonAnalyzer.ts deleted file mode 100644 index 16dd4b9301..0000000000 --- a/packages/cli/src/commands/onboard/discovery/analyzers/PackageJsonAnalyzer.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2023 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 { - ANNOTATION_SOURCE_LOCATION, - ComponentEntity, -} from '@backstage/catalog-model'; -import z from 'zod'; -import { AnalysisOutputs, Analyzer } from './types'; -import { Repository, RepositoryFile } from '../providers/types'; - -export class PackageJsonAnalyzer implements Analyzer { - name(): string { - return PackageJsonAnalyzer.name; - } - - async analyzeRepository(options: { - repository: Repository; - output: AnalysisOutputs; - }): Promise { - const packageJson = await options.repository.file('package.json'); - if (!packageJson) { - return; - } - - const content = await readPackageJson(packageJson); - if (!content) { - return; - } - - const name = sanitizeName(content?.name) ?? options.repository.name; - - const entity: ComponentEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name, - ...(options.repository.description - ? { description: options.repository.description } - : {}), - tags: ['javascript'], - annotations: { - [ANNOTATION_SOURCE_LOCATION]: `url:${options.repository.url}`, - }, - }, - spec: { - type: 'website', - lifecycle: 'production', - owner: 'user:guest', - }, - }; - - const decorate = options.output - .list() - .find(entry => entry.entity.metadata.name === name); - - if (decorate) { - decorate.entity.spec = { - ...decorate.entity.spec, - type: 'website', - }; - - decorate.entity.metadata.tags = [ - ...(decorate.entity.metadata.tags ?? []), - 'javascript', - ]; - - decorate.entity.metadata.annotations = { - ...decorate.entity.metadata.annotations, - [ANNOTATION_SOURCE_LOCATION]: `url:${options.repository.url}`, - }; - - return; - } - - options.output.produce({ - type: 'entity', - path: '/', - entity, - }); - } -} - -const packageSchema = z.object({ - name: z.string().optional(), -}); - -/** - * Makes sure that a name retrieved from a package.json file - * is reasonable and conforms to the catalog naming format. - * - * Read more about the naming format here: - * ADR002: Default Software Catalog File Format - * https://backstage.io/docs/architecture-decisions/adrs-adr002/ - */ -function sanitizeName(name?: string) { - return name && name !== 'root' - ? name.replace(/[^a-z0-9A-Z]/g, '_').substring(0, 62) - : undefined; -} - -async function readPackageJson( - file: RepositoryFile, -): Promise | undefined> { - try { - const text = await file.text(); - const result = packageSchema.safeParse(JSON.parse(text)); - if (!result.success) { - return undefined; - } - return { name: result.data.name }; - } catch (e) { - return undefined; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/analyzers/types.ts b/packages/cli/src/commands/onboard/discovery/analyzers/types.ts deleted file mode 100644 index 57b6f2152b..0000000000 --- a/packages/cli/src/commands/onboard/discovery/analyzers/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2023 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 { Entity } from '@backstage/catalog-model'; -import { Repository } from '../providers/types'; - -export type AnalysisOutput = { - type: 'entity'; - path: string; - entity: Entity; -}; - -export interface AnalysisOutputs { - produce(output: AnalysisOutput): void; - list(): AnalysisOutput[]; -} - -export interface Analyzer { - name(): string; - analyzeRepository(options: { - repository: Repository; - output: AnalysisOutputs; - }): Promise; -} diff --git a/packages/cli/src/commands/onboard/discovery/index.ts b/packages/cli/src/commands/onboard/discovery/index.ts deleted file mode 100644 index 8b2a5b694a..0000000000 --- a/packages/cli/src/commands/onboard/discovery/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2023 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 yaml from 'yaml'; -import inquirer from 'inquirer'; -import chalk from 'chalk'; -import { loadCliConfig } from '../../../lib/config'; -import { updateConfigFile } from '../config'; -import { APP_CONFIG_FILE, DISCOVERED_ENTITIES_FILE } from '../files'; -import { Discovery } from './Discovery'; -import { BasicRepositoryAnalyzer } from './analyzers/BasicRepositoryAnalyzer'; -import { PackageJsonAnalyzer } from './analyzers/PackageJsonAnalyzer'; -import { GithubDiscoveryProvider } from './providers/github/GithubDiscoveryProvider'; -import { GitlabDiscoveryProvider } from './providers/gitlab/GitlabDiscoveryProvider'; -import { GitHubAnswers, GitLabAnswers } from '../auth'; -import { Task } from '../../../lib/tasks'; - -export async function discover(providerInfo?: { - provider: string; - answers: GitHubAnswers | GitLabAnswers; -}) { - Task.log(` - Would you like to scan for - and create - Software Catalog entities? - - You will need to select which SCM (Source Code Management) provider you are using, - and then which repository or organization you want to scan. - - This will generate a new file in the root of your app containing discovered entities, - which will be included in the Software Catalog when you start up Backstage next time. - - Note that this command requires an access token, which can be either added through the integration config or - provided as an environment variable. - `); - - const answers = await inquirer.prompt<{ - shouldContinue: boolean; - provider: string; - url: string; - }>([ - { - type: 'confirm', - name: 'shouldContinue', - message: 'Do you want to continue?', - }, - { - type: 'list', - name: 'provider', - message: 'Please select which SCM provider you want to use:', - choices: ['GitHub', 'GitLab'], - default: providerInfo?.provider, - when: ({ shouldContinue }) => shouldContinue, - }, - { - type: 'input', - name: 'url', - message: `Which repository do you want to scan?`, - when: ({ shouldContinue }) => shouldContinue, - filter: (input, { provider }) => { - if (provider === 'GitLab') { - return `https://gitlab.com/${input}`; - } - if (provider === 'GitHub') { - return `https://github.com/${input}`; - } - return false; - }, - }, - ]); - - if (!answers.shouldContinue) { - Task.log( - chalk.yellow( - 'If you change your mind, feel free to re-run this command.', - ), - ); - return; - } - - const { fullConfig: config } = await loadCliConfig({ args: [] }); - - const discovery = new Discovery(); - - if (answers.provider === 'GitHub') { - discovery.addProvider(GithubDiscoveryProvider.fromConfig(config)); - } - if (answers.provider === 'GitLab') { - discovery.addProvider(GitlabDiscoveryProvider.fromConfig(config)); - } - - discovery.addAnalyzer(new BasicRepositoryAnalyzer()); - discovery.addAnalyzer(new PackageJsonAnalyzer()); - - const { entities } = await discovery.run(answers.url); - - if (!entities.length) { - Task.log( - chalk.yellow(` - We could not find enough information to be able to generate any Software Catalog entities for you. - Perhaps you can try again with a different repository?`), - ); - return; - } - - await Task.forItem('Creating', DISCOVERED_ENTITIES_FILE, async () => { - const payload: string[] = []; - for (const entity of entities) { - payload.push('---\n', yaml.stringify(entity)); - } - await fs.writeFile(DISCOVERED_ENTITIES_FILE, payload.join('')); - }); - - await Task.forItem( - 'Updating', - APP_CONFIG_FILE, - async () => - await updateConfigFile(APP_CONFIG_FILE, { - catalog: { - locations: [ - { - type: 'file', - target: DISCOVERED_ENTITIES_FILE, - }, - ], - }, - }), - ); -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts b/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts deleted file mode 100644 index a24756edd7..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2023 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 { - DefaultGithubCredentialsProvider, - GithubCredentialsProvider, - ScmIntegrations, -} from '@backstage/integration'; -import { graphql } from '@octokit/graphql'; -import { - Repository as GraphqlRepository, - Query as GraphqlQuery, -} from '@octokit/graphql-schema'; -import parseGitUrl from 'git-url-parse'; -import { Provider, Repository } from '../types'; -import { GithubRepository } from './GithubRepository'; - -export class GithubDiscoveryProvider implements Provider { - readonly #envToken: string | undefined; - readonly #scmIntegrations: ScmIntegrations; - readonly #credentialsProvider: GithubCredentialsProvider; - - static fromConfig(config: Config): GithubDiscoveryProvider { - const envToken = process.env.GITHUB_TOKEN || undefined; - const scmIntegrations = ScmIntegrations.fromConfig(config); - const credentialsProvider = - DefaultGithubCredentialsProvider.fromIntegrations(scmIntegrations); - return new GithubDiscoveryProvider( - envToken, - scmIntegrations, - credentialsProvider, - ); - } - - private constructor( - envToken: string | undefined, - integrations: ScmIntegrations, - credentialsProvider: GithubCredentialsProvider, - ) { - this.#envToken = envToken; - this.#scmIntegrations = integrations; - this.#credentialsProvider = credentialsProvider; - } - - name(): string { - return 'GitHub'; - } - - async discover(url: string): Promise { - if (!url.startsWith('https://github.com/')) { - return false; - } - - const scmIntegration = this.#scmIntegrations.github.byUrl(url); - if (!scmIntegration) { - throw new Error(`No GitHub integration found for ${url}`); - } - - const parsed = parseGitUrl(url); - const { name, organization } = parsed; - const org = organization || name; // depends on if it's a repo url or an org url... - - const client = graphql.defaults({ - baseUrl: scmIntegration.config.apiBaseUrl, - headers: await this.#getRequestHeaders(url), - }); - - const { repositories } = await this.#getOrganizationRepositories( - client, - org, - ); - - return repositories - .filter(repo => repo.url.startsWith(url)) - .map(repo => new GithubRepository(client, repo, org)); - } - - async #getRequestHeaders(url: string): Promise> { - const credentials = await this.#credentialsProvider.getCredentials({ - url, - }); - - if (credentials.headers) { - return credentials.headers; - } else if (credentials.token) { - return { authorization: `token ${credentials.token}` }; - } - - if (this.#envToken) { - return { authorization: `token ${this.#envToken}` }; - } - - throw new Error( - 'No token available for GitHub, please configure your integrations or set a GITHUB_TOKEN env variable', - ); - } - - async #getOrganizationRepositories(client: typeof graphql, org: string) { - const query = `query repositories($org: String!, $cursor: String) { - repositoryOwner(login: $org) { - login - repositories(first: 50, after: $cursor) { - nodes { - name - url - description - isArchived - isFork - } - pageInfo { - hasNextPage - endCursor - } - } - } - }`; - - const result: GraphqlRepository[] = []; - - let cursor: string | undefined | null = undefined; - let hasNextPage = true; - - while (hasNextPage) { - const response: GraphqlQuery = await client(query, { - org, - cursor, - }); - - const { repositories: connection } = response.repositoryOwner ?? {}; - - if (!connection) { - throw new Error(`Found no repositories for ${org}`); - } - - for (const repository of connection.nodes ?? []) { - if (repository && !repository.isArchived && !repository.isFork) { - result.push(repository); - } - } - - cursor = connection.pageInfo.endCursor; - hasNextPage = connection.pageInfo.hasNextPage; - } - - return { - repositories: result, - }; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/github/GithubFile.ts b/packages/cli/src/commands/onboard/discovery/providers/github/GithubFile.ts deleted file mode 100644 index 25279b0949..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/github/GithubFile.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 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 { RepositoryFile } from '../types'; - -export class GithubFile implements RepositoryFile { - readonly #path: string; - readonly #content: string; - - constructor(path: string, content: string) { - this.#path = path; - this.#content = content; - } - - get path(): string { - return this.#path; - } - - async text(): Promise { - return this.#content; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/github/GithubRepository.ts b/packages/cli/src/commands/onboard/discovery/providers/github/GithubRepository.ts deleted file mode 100644 index f3c68851a0..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/github/GithubRepository.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2023 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 { graphql } from '@octokit/graphql'; -import { - Repository as GraphqlRepository, - Blob as GraphqlBlob, -} from '@octokit/graphql-schema'; -import { Repository, RepositoryFile } from '../types'; -import { GithubFile } from './GithubFile'; - -export class GithubRepository implements Repository { - readonly #client: typeof graphql; - readonly #repo: GraphqlRepository; - readonly #org: string; - - constructor(client: typeof graphql, repo: GraphqlRepository, org: string) { - this.#client = client; - this.#repo = repo; - this.#org = org; - } - - get url(): string { - return this.#repo.url; - } - - get name(): string { - return this.#repo.name; - } - - get owner(): string { - return this.#org; - } - - get description(): string | undefined { - return this.#repo.description ?? undefined; - } - - async file(filename: string): Promise { - const content = await this.#getFileContent(filename); - if (!content || content.isBinary || !content.text) { - return undefined; - } - - return new GithubFile(filename, content.text ?? ''); - } - - async #getFileContent(filename: string) { - const query = `query RepoFiles($owner: String!, $name: String!, $expr: String!) { - repository(owner: $owner, name: $name) { - object(expression: $expr) { - ...on Blob { - text - isBinary - } - } - } - }`; - - const response = await this.#client<{ repository: GraphqlRepository }>( - query, - { - name: this.#repo.name, - owner: this.#org, - expr: `HEAD:${filename}`, - }, - ); - - return response.repository.object as GraphqlBlob; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabDiscoveryProvider.ts b/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabDiscoveryProvider.ts deleted file mode 100644 index df9ffa4206..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabDiscoveryProvider.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2023 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 { - DefaultGitlabCredentialsProvider, - GitlabCredentialsProvider, - ScmIntegrations, -} from '@backstage/integration'; -import fetch from 'node-fetch'; -import { Provider } from '../types'; -import { GitlabProject, ProjectResponse } from './GitlabProject'; - -export class GitlabDiscoveryProvider implements Provider { - readonly #envToken: string | undefined; - readonly #scmIntegrations: ScmIntegrations; - readonly #credentialsProvider: GitlabCredentialsProvider; - - static fromConfig(config: Config): GitlabDiscoveryProvider { - const envToken = process.env.GITLAB_TOKEN || undefined; - const scmIntegrations = ScmIntegrations.fromConfig(config); - const credentialsProvider = - DefaultGitlabCredentialsProvider.fromIntegrations(scmIntegrations); - - return new GitlabDiscoveryProvider( - envToken, - scmIntegrations, - credentialsProvider, - ); - } - - private constructor( - envToken: string | undefined, - integrations: ScmIntegrations, - credentialsProvider: GitlabCredentialsProvider, - ) { - this.#envToken = envToken; - this.#scmIntegrations = integrations; - this.#credentialsProvider = credentialsProvider; - } - - name(): string { - return 'GitLab'; - } - - async discover(url: string): Promise { - const { origin, pathname } = new URL(url); - const [, user] = pathname.split('/'); - - const scmIntegration = this.#scmIntegrations.gitlab.byUrl(origin); - if (!scmIntegration) { - throw new Error(`No GitLab integration found for ${origin}`); - } - - const headers = await this.#getRequestHeaders(origin); - - const response = await fetch( - `${scmIntegration.config.apiBaseUrl}/users/${user}/projects`, - { headers }, - ); - - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`); - } - - const projects: ProjectResponse[] = await response.json(); - - return projects.map( - project => - new GitlabProject(project, scmIntegration.config.apiBaseUrl, headers), - ); - } - - async #getRequestHeaders(url: string): Promise> { - const credentials = await this.#credentialsProvider.getCredentials({ - url, - }); - - if (credentials.headers) { - return credentials.headers; - } else if (credentials.token) { - return { authorization: `Bearer ${credentials.token}` }; - } - - if (this.#envToken) { - return { authorization: `Bearer ${this.#envToken}` }; - } - - throw new Error( - 'No token available for GitLab, please set a GITLAB_TOKEN env variable', - ); - } -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabFile.ts b/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabFile.ts deleted file mode 100644 index 06f3189ba6..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabFile.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023 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 { RepositoryFile } from '../types'; - -/** - * A single file in a GitLab repository. - */ -export class GitlabFile implements RepositoryFile { - readonly #path: string; - readonly #content: string; - - constructor(path: string, content: string) { - this.#path = path; - this.#content = content; - } - - get path(): string { - return this.#path; - } - - async text(): Promise { - return this.#content; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabProject.ts b/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabProject.ts deleted file mode 100644 index 258e8176bb..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/gitlab/GitlabProject.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2023 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 fetch from 'node-fetch'; -import { GitlabFile } from './GitlabFile'; -import { Repository, RepositoryFile } from '../types'; - -export type ProjectResponse = { - id: string; - name: string; - description: string; - owner: { - username: string; - }; - web_url: string; -}; - -type BranchResponse = { - default: boolean; - name: string; -}; - -type FileContentResponse = { - content: string; -}; - -export class GitlabProject implements Repository { - constructor( - private readonly project: ProjectResponse, - private readonly apiBaseUrl: string, - private readonly headers: { [name: string]: string }, - ) {} - - get url(): string { - return this.project.web_url; - } - - get name(): string { - return this.project.name; - } - - get owner(): string { - return this.project.owner.username; - } - - get description(): string { - return this.project.description; - } - - async file(filename: string): Promise { - const mainBranch = await this.#getMainBranch(); - const content = await this.#getFileContent(filename, mainBranch); - - return new GitlabFile(filename, content); - } - - async #getFileContent(path: string, mainBranch: string): Promise { - const response = await fetch( - `${this.apiBaseUrl}/projects/${this.project.id}/repository/files/${path}?ref=${mainBranch}`, - { headers: this.headers }, - ); - const { content }: FileContentResponse = await response.json(); - - return Buffer.from(content, 'base64').toString('ascii'); - } - - async #getMainBranch(): Promise { - const response = await fetch( - `${this.apiBaseUrl}/projects/${this.project.id}/repository/branches`, - { headers: this.headers }, - ); - const branches: BranchResponse[] = await response.json(); - - return branches.find(branch => branch.default)?.name ?? 'main'; - } -} diff --git a/packages/cli/src/commands/onboard/discovery/providers/types.ts b/packages/cli/src/commands/onboard/discovery/providers/types.ts deleted file mode 100644 index 2d8ce70ecf..0000000000 --- a/packages/cli/src/commands/onboard/discovery/providers/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2023 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. - */ - -/** - * Abstraction for a single repository. - */ -export interface Repository { - url: string; - - name: string; - - owner: string; - - description?: string; - - file(filename: string): Promise; -} - -/** - * Abstraction for a single repository file. - */ -export interface RepositoryFile { - /** - * The filepath of the data. - */ - path: string; - - /** - * The textual contents of the file. - */ - text(): Promise; -} - -/** - * One integration that supports discovery of repositories. - */ -export interface Provider { - name(): string; - discover(url: string): Promise; -} diff --git a/packages/cli/src/commands/onboard/files.ts b/packages/cli/src/commands/onboard/files.ts deleted file mode 100644 index febec35d4a..0000000000 --- a/packages/cli/src/commands/onboard/files.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 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 { findPaths } from '@backstage/cli-common'; -import * as path from 'path'; - -/* eslint-disable-next-line no-restricted-syntax */ -const { targetRoot, ownDir } = findPaths(__dirname); - -export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); -export const DISCOVERED_ENTITIES_FILE = path.join( - targetRoot, - 'examples', - 'discovered-entities.yaml', -); -export const PATCH_FOLDER = path.join( - ownDir, - 'src', - 'commands', - 'onboard', - 'auth', - 'patches', -); diff --git a/packages/cli/src/commands/onboard/index.ts b/packages/cli/src/commands/onboard/index.ts deleted file mode 100644 index 4c23ea6e48..0000000000 --- a/packages/cli/src/commands/onboard/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2023 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 { command } from './command'; diff --git a/packages/cli/src/commands/onboard/integrations/github.ts b/packages/cli/src/commands/onboard/integrations/github.ts deleted file mode 100644 index a0538c7ace..0000000000 --- a/packages/cli/src/commands/onboard/integrations/github.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2023 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 chalk from 'chalk'; -import inquirer from 'inquirer'; -import { Task } from '../../../lib/tasks'; -import { updateConfigFile } from '../config'; -import { APP_CONFIG_FILE } from '../files'; -import { GitHubAnswers } from '../auth'; - -type Answers = { - hasEnterprise: boolean; - enterpriseInstanceUrl: string; - apiBaseUrl: string; -}; - -const getConfig = ({ - hasEnterprise, - apiBaseUrl, - host, - token, -}: { - hasEnterprise: boolean; - apiBaseUrl: string; - host: string; - token: string; -}) => ({ - integrations: { - github: [ - { - host, - token, - ...(hasEnterprise && { - apiBaseUrl, - }), - }, - ], - }, -}); - -export const github = async (providerAnswers?: GitHubAnswers) => { - // TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication? - const answers = await inquirer.prompt([ - { - type: 'confirm', - name: 'hasEnterprise', - message: 'Are you using GitHub Enterprise?', - when: () => typeof providerAnswers === 'undefined', - }, - { - type: 'input', - name: 'enterpriseInstanceUrl', - message: 'What is your URL for GitHub Enterprise?', - when: ({ hasEnterprise }) => hasEnterprise, - validate: (input: string) => Boolean(new URL(input)), - }, - { - type: 'input', - name: 'apiBaseUrl', - message: 'What is your GitHub Enterprise API path?', - default: '/api/v3', - when: ({ hasEnterprise }) => - hasEnterprise || providerAnswers?.hasEnterprise, - // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up - }, - ]); - - const host = new URL( - providerAnswers?.enterpriseInstanceUrl ?? - answers?.enterpriseInstanceUrl ?? - 'http://github.com', - ); - - Task.log(` - To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue( - `${host.origin}/settings/tokens/new`, - )} - - Select the following scopes: - - Reading software components:${chalk.cyan(` - - "repo"`)} - - Reading organization data:${chalk.cyan(` - - "read:org" - - "read:user" - - "user:email"`)} - - Publishing software templates:${chalk.cyan(` - - "repo" - - "workflow" (if templates include GitHub workflows) - `)} - - You can find the full documentation page here: ${chalk.blue( - 'https://backstage.io/docs/integrations/github/locations', - )} - `); - - const { token } = await inquirer.prompt<{ token: string }>([ - { - type: 'input', - name: 'token', - message: - 'Please insert your personal access token to setup the GitHub Integration', - // TODO(tudi2d): validate - }, - ]); - - const config = getConfig({ - hasEnterprise: providerAnswers?.hasEnterprise ?? answers.hasEnterprise, - apiBaseUrl: host.origin + answers.apiBaseUrl, - host: host.hostname, - token, - }); - - Task.log('Setting up Software Templates using GitHub integration for you...'); - - await Task.forItem( - 'Updating', - APP_CONFIG_FILE, - async () => await updateConfigFile(APP_CONFIG_FILE, config), - ); -}; diff --git a/packages/cli/src/commands/onboard/integrations/index.ts b/packages/cli/src/commands/onboard/integrations/index.ts deleted file mode 100644 index 5cb2f95b38..0000000000 --- a/packages/cli/src/commands/onboard/integrations/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2023 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 inquirer from 'inquirer'; -import { Task } from '../../../lib/tasks'; -import { GitHubAnswers, GitLabAnswers } from '../auth'; -import { github } from './github'; - -enum Integration { - GITHUB = 'GitHub', -} - -const Integrations: Integration[] = [Integration.GITHUB]; - -export async function integrations(providerInfo?: { - provider: string; - answers: GitHubAnswers | GitLabAnswers; -}): Promise { - const answers = await inquirer.prompt<{ - integration?: Integration; - shouldUsePreviousProvider: boolean; - }>([ - { - type: 'confirm', - name: 'shouldUsePreviousProvider', - message: `Do you want to keep using ${providerInfo?.provider} as your provider when setting up Software Templates?`, - when: () => - providerInfo?.provider && - Object.values(Integrations).includes( - providerInfo!.provider as Integration, - ), - }, - { - // TODO(tudi2d): Let's start with one, but it should be multiple choice in the future - type: 'list', - name: 'integration', - message: 'Please select an integration provider:', - choices: Integrations, - when: ({ shouldUsePreviousProvider }) => !shouldUsePreviousProvider, - }, - ]); - - if (answers.shouldUsePreviousProvider) { - answers.integration = providerInfo!.provider as Integration; - } - - switch (answers.integration) { - case Integration.GITHUB: { - const providerAnswers = - providerInfo?.provider === 'GitHub' - ? (providerInfo!.answers as GitHubAnswers) - : undefined; - await github(providerAnswers); - break; - } - default: - } - - Task.log(); - Task.log(`Done setting up ${answers.integration} Integration!`); - Task.log(); -}