commit 1a4ef09d259836cf07cb205338ec8f53985f5066 Author: kjuulh Date: Thu Dec 22 15:01:37 2022 +0100 base diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..505a7b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +node_modules +packages/*/src +packages/*/node_modules +plugins +*.local.yaml diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..e351352 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + root: true, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d16a8d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# macOS +.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Coverage directory generated when running tests with coverage +coverage + +# Dependencies +node_modules/ + +# Node version directives +.nvmrc + +# dotenv environment variables file +.env +.env.test + +# Build output +dist +dist-types + +# Temporary change files created by Vim +*.swp + +# MkDocs build output +site + +# Local configuration files +*.local.yaml + +# Sensitive credentials +*-credentials.yaml diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dfb0f1c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist +dist-types +coverage +.vscode diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c7c437 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# [Backstage](https://backstage.io) + +This is your newly scaffolded Backstage App, Good Luck! + +To start the app, run: + +```sh +yarn install +yarn dev +``` diff --git a/app-config.production.yaml b/app-config.production.yaml new file mode 100644 index 0000000..6535d96 --- /dev/null +++ b/app-config.production.yaml @@ -0,0 +1,40 @@ +app: + # Should be the same as backend.baseUrl when using the `app-backend` plugin. + baseUrl: http://localhost:7007 + +backend: + # Note that the baseUrl should be the URL that the browser and other clients + # should use when communicating with the backend, i.e. it needs to be + # reachable not just from within the backend host, but from all of your + # callers. When its value is "http://localhost:7007", it's strictly private + # and can't be reached by others. + baseUrl: http://localhost:7007 + listen: + port: 7007 + # The following host directive binds to all IPv4 interfaces when its value + # is "0.0.0.0". This is the most permissive setting. The right value depends + # on your specific deployment. If you remove the host line entirely, the + # backend will bind on the interface that corresponds to the backend.baseUrl + # hostname. + host: 0.0.0.0 + + # config options: https://node-postgres.com/api/client + database: + client: pg + connection: + host: ${POSTGRES_HOST} + port: ${POSTGRES_PORT} + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + # https://node-postgres.com/features/ssl + # you can set the sslmode configuration option via the `PGSSLMODE` environment variable + # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) + # ssl: + # ca: # if you have a CA file and want to verify it you can uncomment this section + # $file: /ca/server.crt + +catalog: + # Overrides the default list locations from app-config.yaml as these contain example data. + # See https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog for more details + # on how to get entities into the catalog. + locations: [] diff --git a/app-config.yaml b/app-config.yaml new file mode 100644 index 0000000..cd6998d --- /dev/null +++ b/app-config.yaml @@ -0,0 +1,103 @@ +app: + title: Scaffolded Backstage App + baseUrl: http://localhost:3000 + +organization: + name: My Company + +backend: + # Used for enabling authentication, secret is shared by all backend plugins + # See https://backstage.io/docs/tutorials/backend-to-backend-auth for + # information on the format + # auth: + # keys: + # - secret: ${BACKEND_SECRET} + baseUrl: http://localhost:7007 + listen: + port: 7007 + # Uncomment the following host directive to bind to all IPv4 interfaces and + # not just the baseUrl hostname. + # host: 0.0.0.0 + csp: + connect-src: ["'self'", 'http:', 'https:'] + # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference + # Default Helmet Content-Security-Policy values can be removed by setting the key to false + cors: + origin: http://localhost:3000 + methods: [GET, POST, PUT, DELETE] + credentials: true + # This is for local developement only, it is not recommended to use this in production + # The production database configuration is stored in app-config.production.yaml + database: + client: better-sqlite3 + connection: ':memory:' + cache: + store: memory + # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir + +integrations: + github: + - host: github.com + # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information + # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration + token: ${GITHUB_TOKEN} + ### Example for how to add your GitHub Enterprise instance using the API: + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: ${GHE_TOKEN} + +proxy: + '/test': + target: 'https://example.com' + changeOrigin: true + +# Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach +techdocs: + builder: 'local' # Alternatives - 'external' + generator: + runIn: 'docker' # Alternatives - 'local' + publisher: + type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. + +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + providers: {} + +scaffolder: + # see https://backstage.io/docs/features/software-templates/configuration for software template options + +catalog: + import: + entityFilename: catalog-info.yaml + pullRequestBranchName: backstage-integration + rules: + - allow: [Component, System, API, Resource, Location] + locations: + # Local example data, file locations are relative to the backend process, typically `packages/backend` + - type: file + target: ../../examples/entities.yaml + + # Local example template + - type: file + target: ../../examples/template/template.yaml + rules: + - allow: [Template] + + # Local example organizational data + - type: file + target: ../../examples/org.yaml + rules: + - allow: [User, Group] + + ## Uncomment these lines to add more example data + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml + + ## Uncomment these lines to add an example org + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml + # rules: + # - allow: [User, Group] diff --git a/backstage.json b/backstage.json new file mode 100644 index 0000000..b23367d --- /dev/null +++ b/backstage.json @@ -0,0 +1,3 @@ +{ + "version": "1.3.0" +} diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..a95ac61 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,13 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: nefarious + description: An example of a Backstage application. + # Example for optional annotations + # annotations: + # github.com/project-slug: backstage/backstage + # backstage.io/techdocs-ref: dir:. +spec: + type: website + owner: john@example.com + lifecycle: experimental diff --git a/examples/entities.yaml b/examples/entities.yaml new file mode 100644 index 0000000..447e8b1 --- /dev/null +++ b/examples/entities.yaml @@ -0,0 +1,41 @@ +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system +apiVersion: backstage.io/v1alpha1 +kind: System +metadata: + name: examples +spec: + owner: guests +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-website +spec: + type: website + lifecycle: experimental + owner: guests + system: examples + providesApis: [example-grpc-api] +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: example-grpc-api +spec: + type: grpc + lifecycle: experimental + owner: guests + system: examples + definition: | + syntax = "proto3"; + + service Exampler { + rpc Example (ExampleMessage) returns (ExampleMessage) {}; + } + + message ExampleMessage { + string example = 1; + }; diff --git a/examples/org.yaml b/examples/org.yaml new file mode 100644 index 0000000..a10e81f --- /dev/null +++ b/examples/org.yaml @@ -0,0 +1,17 @@ +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: guest +spec: + memberOf: [guests] +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: guests +spec: + type: team + children: [] diff --git a/examples/template/content/catalog-info.yaml b/examples/template/content/catalog-info.yaml new file mode 100644 index 0000000..d4ccca4 --- /dev/null +++ b/examples/template/content/catalog-info.yaml @@ -0,0 +1,8 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: ${{ values.name | dump }} +spec: + type: service + owner: user:guest + lifecycle: experimental diff --git a/examples/template/content/index.js b/examples/template/content/index.js new file mode 100644 index 0000000..071ce5a --- /dev/null +++ b/examples/template/content/index.js @@ -0,0 +1 @@ +console.log('Hello from ${{ values.name }}!'); diff --git a/examples/template/content/package.json b/examples/template/content/package.json new file mode 100644 index 0000000..86f968a --- /dev/null +++ b/examples/template/content/package.json @@ -0,0 +1,5 @@ +{ + "name": "${{ values.name }}", + "private": true, + "dependencies": {} +} diff --git a/examples/template/template.yaml b/examples/template/template.yaml new file mode 100644 index 0000000..50052b7 --- /dev/null +++ b/examples/template/template.yaml @@ -0,0 +1,74 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template +kind: Template +metadata: + name: example-nodejs-template + title: Example Node.js Template + description: An example template for the scaffolder that creates a simple Node.js service +spec: + owner: user:guest + type: service + + # These parameters are used to generate the input form in the frontend, and are + # used to gather input data for the execution of the template. + parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + + # These steps are executed in the scaffolder backend, using data that we gathered + # via the parameters above. + steps: + # Each step executes an action, in this case one templates files into the working directory. + - id: fetch-base + name: Fetch Base + action: fetch:template + input: + url: ./content + values: + name: ${{ parameters.name }} + + # This step publishes the contents of the working directory to GitHub. + - id: publish + name: Publish + action: publish:github + input: + allowedHosts: ['github.com'] + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} + + # The final step is to register our new component in the catalog. + - id: register + name: Register + action: catalog:register + input: + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} + catalogInfoPath: '/catalog-info.yaml' + + # Outputs are displayed to the user after a successful execution of the template. + output: + links: + - title: Repository + url: ${{ steps.publish.output.remoteUrl }} + - title: Open in catalog + icon: catalog + entityRef: ${{ steps.register.output.entityRef }} diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..322929d --- /dev/null +++ b/lerna.json @@ -0,0 +1,6 @@ +{ + "packages": ["packages/*", "plugins/*"], + "npmClient": "yarn", + "useWorkspaces": true, + "version": "0.1.0" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bfc2fd7 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "root", + "version": "1.0.0", + "private": true, + "engines": { + "node": "14 || 16" + }, + "scripts": { + "dev": "concurrently \"yarn start\" \"yarn start-backend\"", + "start": "yarn workspace app start", + "start-backend": "yarn workspace backend start", + "build": "backstage-cli repo build --all", + "build-image": "yarn workspace backend build-image", + "tsc": "tsc", + "tsc:full": "tsc --skipLibCheck false --incremental false", + "clean": "backstage-cli clean && lerna run clean", + "diff": "lerna run diff --", + "test": "backstage-cli test", + "test:all": "lerna run test -- --coverage", + "lint": "backstage-cli repo lint --since origin/master", + "lint:all": "backstage-cli repo lint", + "prettier:check": "prettier --check .", + "create-plugin": "backstage-cli create-plugin --scope internal", + "remove-plugin": "backstage-cli remove-plugin" + }, + "workspaces": { + "packages": [ + "packages/*", + "plugins/*" + ] + }, + "devDependencies": { + "@backstage/cli": "^0.17.2", + "@spotify/prettier-config": "^12.0.0", + "concurrently": "^6.0.0", + "lerna": "^4.0.0", + "prettier": "^2.3.2", + "typescript": "~4.6.4" + }, + "resolutions": { + "@types/react": "^17", + "@types/react-dom": "^17" + }, + "prettier": "@spotify/prettier-config", + "lint-staged": { + "*.{js,jsx,ts,tsx,mjs,cjs}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md}": [ + "prettier --write" + ] + } +} diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 0000000..6327fa0 --- /dev/null +++ b/packages/README.md @@ -0,0 +1,9 @@ +# The Packages Folder + +This is where your own applications and centrally managed libraries live, each +in a separate folder of its own. + +From the start there's an `app` folder (for the frontend) and a `backend` folder +(for the Node backend), but you can also add more modules in here that house +your core additions and adaptations, such as themes, common React component +libraries, utilities, and similar. diff --git a/packages/app/.eslintrc.js b/packages/app/.eslintrc.js new file mode 100644 index 0000000..e2a53a6 --- /dev/null +++ b/packages/app/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app/cypress.json b/packages/app/cypress.json new file mode 100644 index 0000000..0cb845a --- /dev/null +++ b/packages/app/cypress.json @@ -0,0 +1,6 @@ +{ + "baseUrl": "http://localhost:3001", + "fixturesFolder": false, + "pluginsFile": false, + "retries": 3 +} diff --git a/packages/app/cypress/.eslintrc.json b/packages/app/cypress/.eslintrc.json new file mode 100644 index 0000000..2b3a458 --- /dev/null +++ b/packages/app/cypress/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "plugins": ["cypress"], + "extends": ["plugin:cypress/recommended"], + "rules": { + "jest/expect-expect": [ + "error", + { + "assertFunctionNames": ["expect", "cy.contains"] + } + ], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true, + "optionalDependencies": true, + "peerDependencies": true, + "bundledDependencies": true + } + ] + } +} diff --git a/packages/app/cypress/integration/app.js b/packages/app/cypress/integration/app.js new file mode 100644 index 0000000..43fb2e3 --- /dev/null +++ b/packages/app/cypress/integration/app.js @@ -0,0 +1,6 @@ +describe('App', () => { + it('should render the catalog', () => { + cy.visit('/'); + cy.contains('My Company Catalog'); + }); +}); diff --git a/packages/app/package.json b/packages/app/package.json new file mode 100644 index 0000000..f4cbcda --- /dev/null +++ b/packages/app/package.json @@ -0,0 +1,83 @@ +{ + "name": "app", + "version": "0.0.0", + "private": true, + "bundled": true, + "backstage": { + "role": "frontend" + }, + "dependencies": { + "@backstage/app-defaults": "^1.0.3", + "@backstage/catalog-model": "^1.0.3", + "@backstage/cli": "^0.17.2", + "@backstage/core-app-api": "^1.0.3", + "@backstage/core-components": "^0.9.5", + "@backstage/core-plugin-api": "^1.0.3", + "@backstage/integration-react": "^1.1.1", + "@backstage/plugin-api-docs": "^0.8.6", + "@backstage/plugin-catalog": "^1.3.0", + "@backstage/plugin-catalog-common": "^1.0.3", + "@backstage/plugin-catalog-graph": "^0.2.18", + "@backstage/plugin-catalog-import": "^0.8.9", + "@backstage/plugin-catalog-react": "^1.1.1", + "@backstage/plugin-github-actions": "^0.5.6", + "@backstage/plugin-org": "^0.5.6", + "@backstage/plugin-permission-react": "^0.4.2", + "@backstage/plugin-scaffolder": "^1.3.0", + "@backstage/plugin-search": "^0.9.0", + "@backstage/plugin-search-react": "^0.2.1", + "@backstage/plugin-tech-radar": "^0.5.13", + "@backstage/plugin-techdocs": "^1.2.0", + "@backstage/plugin-techdocs-react": "^1.0.1", + "@backstage/plugin-techdocs-module-addons-contrib": "^1.0.1", + "@backstage/plugin-user-settings": "^0.4.5", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "history": "^5.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/test-utils": "^1.1.1", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/react-dom": "*", + "cross-env": "^7.0.0", + "cypress": "^9.7.0", + "eslint-plugin-cypress": "^2.10.3", + "start-server-and-test": "^1.10.11" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint", + "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev", + "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run", + "cy:dev": "cypress open", + "cy:run": "cypress run --browser chrome" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "files": [ + "dist" + ] +} diff --git a/packages/app/public/android-chrome-192x192.png b/packages/app/public/android-chrome-192x192.png new file mode 100644 index 0000000..eec0ae2 Binary files /dev/null and b/packages/app/public/android-chrome-192x192.png differ diff --git a/packages/app/public/apple-touch-icon.png b/packages/app/public/apple-touch-icon.png new file mode 100644 index 0000000..3158830 Binary files /dev/null and b/packages/app/public/apple-touch-icon.png differ diff --git a/packages/app/public/favicon-16x16.png b/packages/app/public/favicon-16x16.png new file mode 100644 index 0000000..58cf61a Binary files /dev/null and b/packages/app/public/favicon-16x16.png differ diff --git a/packages/app/public/favicon-32x32.png b/packages/app/public/favicon-32x32.png new file mode 100644 index 0000000..c0915ec Binary files /dev/null and b/packages/app/public/favicon-32x32.png differ diff --git a/packages/app/public/favicon.ico b/packages/app/public/favicon.ico new file mode 100644 index 0000000..5e45e5d Binary files /dev/null and b/packages/app/public/favicon.ico differ diff --git a/packages/app/public/index.html b/packages/app/public/index.html new file mode 100644 index 0000000..a936c73 --- /dev/null +++ b/packages/app/public/index.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + <%= config.getString('app.title') %> + <% if (config.has('app.googleAnalyticsTrackingId')) { %> + + + <% } %> + + + +
+ + + diff --git a/packages/app/public/manifest.json b/packages/app/public/manifest.json new file mode 100644 index 0000000..4a7c1b4 --- /dev/null +++ b/packages/app/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Backstage", + "name": "Backstage", + "icons": [ + { + "src": "favicon.ico", + "sizes": "48x48", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/app/public/robots.txt b/packages/app/public/robots.txt new file mode 100644 index 0000000..01b0f9a --- /dev/null +++ b/packages/app/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/packages/app/public/safari-pinned-tab.svg b/packages/app/public/safari-pinned-tab.svg new file mode 100644 index 0000000..0f500b3 --- /dev/null +++ b/packages/app/public/safari-pinned-tab.svg @@ -0,0 +1 @@ +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx new file mode 100644 index 0000000..b94cac7 --- /dev/null +++ b/packages/app/src/App.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { renderWithEffects } from '@backstage/test-utils'; +import App from './App'; + +describe('App', () => { + it('should render', async () => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { title: 'Test' }, + backend: { baseUrl: 'http://localhost:7007' }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + }, + context: 'test', + }, + ] as any, + }; + + const rendered = await renderWithEffects(); + expect(rendered.baseElement).toBeInTheDocument(); + }); +}); diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx new file mode 100644 index 0000000..c487726 --- /dev/null +++ b/packages/app/src/App.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Navigate, Route } from 'react-router'; +import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; +import { + CatalogEntityPage, + CatalogIndexPage, + catalogPlugin, +} from '@backstage/plugin-catalog'; +import { + CatalogImportPage, + catalogImportPlugin, +} from '@backstage/plugin-catalog-import'; +import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { orgPlugin } from '@backstage/plugin-org'; +import { SearchPage } from '@backstage/plugin-search'; +import { TechRadarPage } from '@backstage/plugin-tech-radar'; +import { + TechDocsIndexPage, + techdocsPlugin, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { UserSettingsPage } from '@backstage/plugin-user-settings'; +import { apis } from './apis'; +import { entityPage } from './components/catalog/EntityPage'; +import { searchPage } from './components/search/SearchPage'; +import { Root } from './components/Root'; + +import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; +import { PermissionedRoute } from '@backstage/plugin-permission-react'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; + +const app = createApp({ + apis, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(apiDocsPlugin.externalRoutes, { + registerApi: catalogImportPlugin.routes.importPage, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + bind(orgPlugin.externalRoutes, { + catalogIndex: catalogPlugin.routes.catalogIndex, + }); + }, +}); + +const AppProvider = app.getProvider(); +const AppRouter = app.getRouter(); + +const routes = ( + + + } /> + } + > + {entityPage} + + } /> + } + > + + + + + } /> + } /> + } + /> + } + /> + }> + {searchPage} + + } /> + } /> + +); + +const App = () => ( + + + + + {routes} + + +); + +export default App; diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts new file mode 100644 index 0000000..c89753a --- /dev/null +++ b/packages/app/src/apis.ts @@ -0,0 +1,19 @@ +import { + ScmIntegrationsApi, + scmIntegrationsApiRef, + ScmAuth, +} from '@backstage/integration-react'; +import { + AnyApiFactory, + configApiRef, + createApiFactory, +} from '@backstage/core-plugin-api'; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: scmIntegrationsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), + }), + ScmAuth.createDefaultApiFactory(), +]; diff --git a/packages/app/src/components/Root/LogoFull.tsx b/packages/app/src/components/Root/LogoFull.tsx new file mode 100644 index 0000000..c7b1c84 --- /dev/null +++ b/packages/app/src/components/Root/LogoFull.tsx @@ -0,0 +1,46 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 30, + }, + path: { + fill: '#7df3e1', + }, +}); +const LogoFull = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoFull; diff --git a/packages/app/src/components/Root/LogoIcon.tsx b/packages/app/src/components/Root/LogoIcon.tsx new file mode 100644 index 0000000..073cf6e --- /dev/null +++ b/packages/app/src/components/Root/LogoIcon.tsx @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 28, + }, + path: { + fill: '#7df3e1', + }, +}); + +const LogoIcon = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoIcon; diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx new file mode 100644 index 0000000..b1164a3 --- /dev/null +++ b/packages/app/src/components/Root/Root.tsx @@ -0,0 +1,112 @@ +/* + * 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 React, { PropsWithChildren } from 'react'; +import { Link, makeStyles } from '@material-ui/core'; +import HomeIcon from '@material-ui/icons/Home'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import MapIcon from '@material-ui/icons/MyLocation'; +import LibraryBooks from '@material-ui/icons/LibraryBooks'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import LogoFull from './LogoFull'; +import LogoIcon from './LogoIcon'; +import { NavLink } from 'react-router-dom'; +import { + Settings as SidebarSettings, + UserSettingsSignInAvatar, +} from '@backstage/plugin-user-settings'; +import { SidebarSearchModal } from '@backstage/plugin-search'; +import { + Sidebar, + sidebarConfig, + SidebarDivider, + SidebarGroup, + SidebarItem, + SidebarPage, + SidebarScrollWrapper, + SidebarSpace, + useSidebarOpenState, +} from '@backstage/core-components'; +import MenuIcon from '@material-ui/icons/Menu'; +import SearchIcon from '@material-ui/icons/Search'; + +const useSidebarLogoStyles = makeStyles({ + root: { + width: sidebarConfig.drawerWidthClosed, + height: 3 * sidebarConfig.logoHeight, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + marginBottom: -14, + }, + link: { + width: sidebarConfig.drawerWidthClosed, + marginLeft: 24, + }, +}); + +const SidebarLogo = () => { + const classes = useSidebarLogoStyles(); + const { isOpen } = useSidebarOpenState(); + + return ( +
+ + {isOpen ? : } + +
+ ); +}; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + } to="/search"> + + + + }> + {/* Global nav, not org-specific */} + + + + + {/* End global nav */} + + + + + + + + } + to="/settings" + > + + + + {children} + +); diff --git a/packages/app/src/components/Root/index.ts b/packages/app/src/components/Root/index.ts new file mode 100644 index 0000000..dff706f --- /dev/null +++ b/packages/app/src/components/Root/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { Root } from './Root'; diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx new file mode 100644 index 0000000..6ec4da0 --- /dev/null +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -0,0 +1,390 @@ +/* + * 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 React from 'react'; +import { Button, Grid } from '@material-ui/core'; +import { + EntityApiDefinitionCard, + EntityConsumedApisCard, + EntityConsumingComponentsCard, + EntityHasApisCard, + EntityProvidedApisCard, + EntityProvidingComponentsCard, +} from '@backstage/plugin-api-docs'; +import { + EntityAboutCard, + EntityDependsOnComponentsCard, + EntityDependsOnResourcesCard, + EntityHasComponentsCard, + EntityHasResourcesCard, + EntityHasSubcomponentsCard, + EntityHasSystemsCard, + EntityLayout, + EntityLinksCard, + EntitySwitch, + EntityOrphanWarning, + EntityProcessingErrorsPanel, + isComponentType, + isKind, + hasCatalogProcessingErrors, + isOrphan, +} from '@backstage/plugin-catalog'; +import { + isGithubActionsAvailable, + EntityGithubActionsContent, +} from '@backstage/plugin-github-actions'; +import { + EntityUserProfileCard, + EntityGroupProfileCard, + EntityMembersListCard, + EntityOwnershipCard, +} from '@backstage/plugin-org'; +import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; +import { EmptyState } from '@backstage/core-components'; +import { + Direction, + EntityCatalogGraphCard, +} from '@backstage/plugin-catalog-graph'; +import { + RELATION_API_CONSUMED_BY, + RELATION_API_PROVIDED_BY, + RELATION_CONSUMES_API, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + RELATION_HAS_PART, + RELATION_PART_OF, + RELATION_PROVIDES_API, +} from '@backstage/catalog-model'; + +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; + +const techdocsContent = ( + + + + + +); + +const cicdContent = ( + // This is an example of how you can implement your company's logic in entity page. + // You can for example enforce that all components of type 'service' should use GitHubActions + + + + + + + + Read more + + } + /> + + +); + +const entityWarningContent = ( + <> + + + + + + + + + + + + + + + + +); + +const overviewContent = ( + + {entityWarningContent} + + + + + + + + + + + + + + +); + +const serviceEntityPage = ( + + + {overviewContent} + + + + {cicdContent} + + + + + + + + + + + + + + + + + + + + + + + + + + {techdocsContent} + + +); + +const websiteEntityPage = ( + + + {overviewContent} + + + + {cicdContent} + + + + + + + + + + + + + + + {techdocsContent} + + +); + +/** + * NOTE: This page is designed to work on small screens such as mobile devices. + * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`, + * since this does not default. If no breakpoints are used, the items will equitably share the available space. + * https://material-ui.com/components/grid/#basic-grid. + */ + +const defaultEntityPage = ( + + + {overviewContent} + + + + {techdocsContent} + + +); + +const componentPage = ( + + + {serviceEntityPage} + + + + {websiteEntityPage} + + + {defaultEntityPage} + +); + +const apiPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +const userPage = ( + + + + {entityWarningContent} + + + + + + + + + +); + +const groupPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + +); + +const systemPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + + + + + + + + + + +); + +const domainPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + +); + +export const entityPage = ( + + + + + + + + + {defaultEntityPage} + +); diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx new file mode 100644 index 0000000..928b820 --- /dev/null +++ b/packages/app/src/components/search/SearchPage.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; + +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + CATALOG_FILTER_EXISTS, +} from '@backstage/plugin-catalog-react'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; + +import { SearchType } from '@backstage/plugin-search'; +import { + DefaultResultListItem, + SearchBar, + SearchFilter, + SearchResult, + useSearch, +} from '@backstage/plugin-search-react'; +import { + CatalogIcon, + Content, + DocsIcon, + Header, + Page, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +const useStyles = makeStyles((theme: Theme) => ({ + bar: { + padding: theme.spacing(1, 0), + }, + filters: { + padding: theme.spacing(2), + marginTop: theme.spacing(2), + }, + filter: { + '& + &': { + marginTop: theme.spacing(2.5), + }, + }, +})); + +const SearchPage = () => { + const classes = useStyles(); + const { types } = useSearch(); + const catalogApi = useApi(catalogApiRef); + + return ( + +
+ + + + + + + + + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ]} + /> + + {types.includes('techdocs') && ( + { + // Return a list of entities which are documented. + const { items } = await catalogApi.getEntities({ + fields: ['metadata.name'], + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + }); + + const names = items.map(entity => entity.metadata.name); + names.sort(); + return names; + }} + /> + )} + + + + + + + {({ results }) => ( + + {results.map(({ type, document, highlight, rank }) => { + switch (type) { + case 'software-catalog': + return ( + + ); + case 'techdocs': + return ( + + ); + default: + return ( + + ); + } + })} + + )} + + + + + + ); +}; + +export const searchPage = ; diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx new file mode 100644 index 0000000..b16aaf7 --- /dev/null +++ b/packages/app/src/index.tsx @@ -0,0 +1,6 @@ +import '@backstage/cli/asset-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/app/src/setupTests.ts b/packages/app/src/setupTests.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/packages/app/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js new file mode 100644 index 0000000..e2a53a6 --- /dev/null +++ b/packages/backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile new file mode 100644 index 0000000..a5773aa --- /dev/null +++ b/packages/backend/Dockerfile @@ -0,0 +1,34 @@ +# This dockerfile builds an image for the backend package. +# It should be executed with the root of the repo as docker context. +# +# Before building this image, be sure to have run the following commands in the repo root: +# +# yarn install +# yarn tsc +# yarn build +# +# Once the commands have been run, you can build the image using `yarn build-image` + +FROM node:16-bullseye-slim + +WORKDIR /app + +# install sqlite3 dependencies, you can skip this if you don't use sqlite3 in the image +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + +# Copy repo skeleton first, to avoid unnecessary docker cache invalidation. +# The skeleton contains the package.json of each package in the monorepo, +# and along with yarn.lock and the root package.json, that's enough to run yarn install. +COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ +RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + +RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" + +# Then copy the rest of the backend bundle, along with any other files we might want. +COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./ +RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] diff --git a/packages/backend/README.md b/packages/backend/README.md new file mode 100644 index 0000000..02426ef --- /dev/null +++ b/packages/backend/README.md @@ -0,0 +1,66 @@ +# example-backend + +This package is an EXAMPLE of a Backstage backend. + +The main purpose of this package is to provide a test bed for Backstage plugins +that have a backend part. Feel free to experiment locally or within your fork by +adding dependencies and routes to this backend, to try things out. + +Our goal is to eventually amend the create-app flow of the CLI, such that a +production ready version of a backend skeleton is made alongside the frontend +app. Until then, feel free to experiment here! + +## Development + +To run the example backend, first go to the project root and run + +```bash +yarn install +yarn tsc +yarn build +``` + +You should only need to do this once. + +After that, go to the `packages/backend` directory and run + +```bash +AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \ +AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \ +AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \ +AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \ +LOG_LEVEL=debug \ +yarn start +``` + +Substitute `x` for actual values, or leave them as dummy values just to try out +the backend without using the auth or sentry features. + +The backend starts up on port 7007 per default. + +## Populating The Catalog + +If you want to use the catalog functionality, you need to add so called +locations to the backend. These are places where the backend can find some +entity descriptor data to consume and serve. For more information, see +[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog). + +To get started quickly, this template already includes some statically configured example locations +in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you +like, and also override them for local development in `app-config.local.yaml`. + +## Authentication + +We chose [Passport](http://www.passportjs.org/) as authentication platform due +to its comprehensive set of supported authentication +[strategies](http://www.passportjs.org/packages/). + +Read more about the +[auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) +and +[how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) diff --git a/packages/backend/package.json b/packages/backend/package.json new file mode 100644 index 0000000..2e377ca --- /dev/null +++ b/packages/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "backend", + "version": "0.0.0", + "main": "dist/index.cjs.js", + "types": "src/index.ts", + "private": true, + "backstage": { + "role": "backend" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "build-image": "docker build ../.. -f Dockerfile --tag backstage" + }, + "dependencies": { + "app": "link:../app", + "@backstage/backend-common": "^0.14.0", + "@backstage/backend-tasks": "^0.3.2", + "@backstage/catalog-model": "^1.0.3", + "@backstage/catalog-client": "^1.0.3", + "@backstage/config": "^1.0.1", + "@backstage/plugin-app-backend": "^0.3.33", + "@backstage/plugin-auth-backend": "^0.14.1", + "@backstage/plugin-catalog-backend": "^1.2.0", + "@backstage/plugin-permission-common": "^0.6.2", + "@backstage/plugin-permission-node": "^0.6.2", + "@backstage/plugin-proxy-backend": "^0.2.27", + "@backstage/plugin-scaffolder-backend": "^1.3.0", + "@backstage/plugin-search-backend": "^0.5.3", + "@backstage/plugin-search-backend-module-pg": "^0.3.4", + "@backstage/plugin-search-backend-node": "^0.6.2", + "@backstage/plugin-techdocs-backend": "^1.1.2", + "dockerode": "^3.3.1", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "pg": "^8.3.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.17.2", + "@types/dockerode": "^3.3.0", + "@types/express-serve-static-core": "^4.17.5", + "@types/express": "^4.17.6", + "@types/luxon": "^2.0.4", + "better-sqlite3": "^7.5.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend/src/index.test.ts b/packages/backend/src/index.test.ts new file mode 100644 index 0000000..7814b8c --- /dev/null +++ b/packages/backend/src/index.test.ts @@ -0,0 +1,8 @@ +import { PluginEnvironment } from './types'; + +describe('test', () => { + it('unbreaks the test runner', () => { + const unbreaker = {} as PluginEnvironment; + expect(unbreaker).toBeTruthy(); + }); +}); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts new file mode 100644 index 0000000..70bc66b --- /dev/null +++ b/packages/backend/src/index.ts @@ -0,0 +1,109 @@ +/* + * Hi! + * + * Note that this is an EXAMPLE Backstage backend. Please check the README. + * + * Happy hacking! + */ + +import Router from 'express-promise-router'; +import { + createServiceBuilder, + loadBackendConfig, + getRootLogger, + useHotMemoize, + notFoundHandler, + CacheManager, + DatabaseManager, + SingleHostDiscovery, + UrlReaders, + ServerTokenManager, +} from '@backstage/backend-common'; +import { TaskScheduler } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import app from './plugins/app'; +import auth from './plugins/auth'; +import catalog from './plugins/catalog'; +import scaffolder from './plugins/scaffolder'; +import proxy from './plugins/proxy'; +import techdocs from './plugins/techdocs'; +import search from './plugins/search'; +import { PluginEnvironment } from './types'; +import { ServerPermissionClient } from '@backstage/plugin-permission-node'; + +function makeCreateEnv(config: Config) { + const root = getRootLogger(); + const reader = UrlReaders.default({ logger: root, config }); + const discovery = SingleHostDiscovery.fromConfig(config); + const cacheManager = CacheManager.fromConfig(config); + const databaseManager = DatabaseManager.fromConfig(config); + const tokenManager = ServerTokenManager.noop(); + const taskScheduler = TaskScheduler.fromConfig(config); + const permissions = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + + root.info(`Created UrlReader ${reader}`); + + return (plugin: string): PluginEnvironment => { + const logger = root.child({ type: 'plugin', plugin }); + const database = databaseManager.forPlugin(plugin); + const cache = cacheManager.forPlugin(plugin); + const scheduler = taskScheduler.forPlugin(plugin); + return { + logger, + database, + cache, + config, + reader, + discovery, + tokenManager, + scheduler, + permissions, + }; + }; +} + +async function main() { + const config = await loadBackendConfig({ + argv: process.argv, + logger: getRootLogger(), + }); + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); + const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); + const authEnv = useHotMemoize(module, () => createEnv('auth')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + const searchEnv = useHotMemoize(module, () => createEnv('search')); + const appEnv = useHotMemoize(module, () => createEnv('app')); + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + apiRouter.use('/auth', await auth(authEnv)); + apiRouter.use('/techdocs', await techdocs(techdocsEnv)); + apiRouter.use('/proxy', await proxy(proxyEnv)); + apiRouter.use('/search', await search(searchEnv)); + + // Add backends ABOVE this line; this 404 handler is the catch-all fallback + apiRouter.use(notFoundHandler()); + + const service = createServiceBuilder(module) + .loadConfig(config) + .addRouter('/api', apiRouter) + .addRouter('', await app(appEnv)); + + await service.start().catch(err => { + console.log(err); + process.exit(1); + }); +} + +module.hot?.accept(); +main().catch(error => { + console.error(`Backend failed to start up, ${error}`); + process.exit(1); +}); diff --git a/packages/backend/src/plugins/app.ts b/packages/backend/src/plugins/app.ts new file mode 100644 index 0000000..7c37f68 --- /dev/null +++ b/packages/backend/src/plugins/app.ts @@ -0,0 +1,14 @@ +import { createRouter } from '@backstage/plugin-app-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + appPackageName: 'app', + }); +} diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts new file mode 100644 index 0000000..159116d --- /dev/null +++ b/packages/backend/src/plugins/auth.ts @@ -0,0 +1,54 @@ +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, + + // This replaces the default GitHub auth provider with a customized one. + // The `signIn` option enables sign-in for this provider, using the + // identity resolution logic that's provided in the `resolver` callback. + // + // This particular resolver makes all users share a single "guest" identity. + // It should only be used for testing and trying out Backstage. + // + // If you want to use a production ready resolver you can switch to the + // the one that is commented out below, it looks up a user entity in the + // catalog using the GitHub username of the authenticated user. + // That resolver requires you to have user entities populated in the catalog, + // for example using https://backstage.io/docs/integrations/github/org + // + // There are other resolvers to choose from, and you can also create + // your own, see the auth documentation for more details: + // + // https://backstage.io/docs/auth/identity-resolver + github: providers.github.create({ + signIn: { + resolver(_, ctx) { + const userRef = 'user:default/guest'; // Must be a full entity reference + return ctx.issueToken({ + claims: { + sub: userRef, // The user's own identity + ent: [userRef], // A list of identities that the user claims ownership through + }, + }); + }, + // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), + }, + }), + }, + }); +} diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts new file mode 100644 index 0000000..876cb6b --- /dev/null +++ b/packages/backend/src/plugins/catalog.ts @@ -0,0 +1,14 @@ +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; +} diff --git a/packages/backend/src/plugins/proxy.ts b/packages/backend/src/plugins/proxy.ts new file mode 100644 index 0000000..54ec393 --- /dev/null +++ b/packages/backend/src/plugins/proxy.ts @@ -0,0 +1,13 @@ +import { createRouter } from '@backstage/plugin-proxy-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + discovery: env.discovery, + }); +} diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts new file mode 100644 index 0000000..7ce5fcf --- /dev/null +++ b/packages/backend/src/plugins/scaffolder.ts @@ -0,0 +1,20 @@ +import { CatalogClient } from '@backstage/catalog-client'; +import { createRouter } from '@backstage/plugin-scaffolder-backend'; +import { Router } from 'express'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const catalogClient = new CatalogClient({ + discoveryApi: env.discovery, + }); + + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + reader: env.reader, + catalogClient, + }); +} diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts new file mode 100644 index 0000000..e9469dc --- /dev/null +++ b/packages/backend/src/plugins/search.ts @@ -0,0 +1,66 @@ +import { useHotCleanup } from '@backstage/backend-common'; +import { createRouter } from '@backstage/plugin-search-backend'; +import { + IndexBuilder, + LunrSearchEngine, +} from '@backstage/plugin-search-backend-node'; +import { PluginEnvironment } from '../types'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; +import { Router } from 'express'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // Initialize a connection to a search engine. + const searchEngine = new LunrSearchEngine({ + logger: env.logger, + }); + const indexBuilder = new IndexBuilder({ + logger: env.logger, + searchEngine, + }); + + const schedule = env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + // A 3 second delay gives the backend server a chance to initialize before + // any collators are executed, which may attempt requests against the API. + initialDelay: { seconds: 3 }, + }); + + // Collators are responsible for gathering documents known to plugins. This + // collator gathers entities from the software catalog. + indexBuilder.addCollator({ + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + // collator gathers entities from techdocs. + indexBuilder.addCollator({ + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + logger: env.logger, + tokenManager: env.tokenManager, + }), + }); + + // The scheduler controls when documents are gathered from collators and sent + // to the search engine for indexing. + const { scheduler } = await indexBuilder.build(); + scheduler.start(); + + useHotCleanup(module, () => scheduler.stop()); + + return await createRouter({ + engine: indexBuilder.getSearchEngine(), + types: indexBuilder.getDocumentTypes(), + permissions: env.permissions, + config: env.config, + logger: env.logger, + }); +} diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts new file mode 100644 index 0000000..be8bb0c --- /dev/null +++ b/packages/backend/src/plugins/techdocs.ts @@ -0,0 +1,51 @@ +import { DockerContainerRunner } from '@backstage/backend-common'; +import { + createRouter, + Generators, + Preparers, + Publisher, +} from '@backstage/plugin-techdocs-backend'; +import Docker from 'dockerode'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // Preparers are responsible for fetching source files for documentation. + const preparers = await Preparers.fromConfig(env.config, { + logger: env.logger, + reader: env.reader, + }); + + // Docker client (conditionally) used by the generators, based on techdocs.generators config. + const dockerClient = new Docker(); + const containerRunner = new DockerContainerRunner({ dockerClient }); + + // Generators are used for generating documentation sites. + const generators = await Generators.fromConfig(env.config, { + logger: env.logger, + containerRunner, + }); + + // Publisher is used for + // 1. Publishing generated files to storage + // 2. Fetching files from storage and passing them to TechDocs frontend. + const publisher = await Publisher.fromConfig(env.config, { + logger: env.logger, + discovery: env.discovery, + }); + + // checks if the publisher is working and logs the result + await publisher.getReadiness(); + + return await createRouter({ + preparers, + generators, + publisher, + logger: env.logger, + config: env.config, + discovery: env.discovery, + cache: env.cache, + }); +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts new file mode 100644 index 0000000..8e0a864 --- /dev/null +++ b/packages/backend/src/types.ts @@ -0,0 +1,23 @@ +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { + PluginCacheManager, + PluginDatabaseManager, + PluginEndpointDiscovery, + TokenManager, + UrlReader, +} from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; + +export type PluginEnvironment = { + logger: Logger; + database: PluginDatabaseManager; + cache: PluginCacheManager; + config: Config; + reader: UrlReader; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + scheduler: PluginTaskScheduler; + permissions: PermissionEvaluator; +}; diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..58dc32c --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,9 @@ +# The Plugins Folder + +This is where your own plugins and their associated modules live, each in a +separate folder of its own. + +If you want to create a new plugin here, go to your project root directory, run +the command `yarn backstage-cli create`, and follow the on-screen instructions. + +You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ba3f901 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": [ + "packages/*/src", + "plugins/*/src", + "plugins/*/dev", + "plugins/*/migrations" + ], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +}