From 1a4ef09d259836cf07cb205338ec8f53985f5066 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Thu, 22 Dec 2022 15:01:37 +0100 Subject: [PATCH] base --- .dockerignore | 6 + .eslintrc.js | 3 + .gitignore | 39 ++ .prettierignore | 4 + README.md | 10 + app-config.production.yaml | 40 ++ app-config.yaml | 103 +++++ backstage.json | 3 + catalog-info.yaml | 13 + examples/entities.yaml | 41 ++ examples/org.yaml | 17 + examples/template/content/catalog-info.yaml | 8 + examples/template/content/index.js | 1 + examples/template/content/package.json | 5 + examples/template/template.yaml | 74 ++++ lerna.json | 6 + package.json | 54 +++ packages/README.md | 9 + packages/app/.eslintrc.js | 1 + packages/app/cypress.json | 6 + packages/app/cypress/.eslintrc.json | 21 + packages/app/cypress/integration/app.js | 6 + packages/app/package.json | 83 ++++ .../app/public/android-chrome-192x192.png | Bin 0 -> 13599 bytes packages/app/public/apple-touch-icon.png | Bin 0 -> 12619 bytes packages/app/public/favicon-16x16.png | Bin 0 -> 883 bytes packages/app/public/favicon-32x32.png | Bin 0 -> 1686 bytes packages/app/public/favicon.ico | Bin 0 -> 15086 bytes packages/app/public/index.html | 79 ++++ packages/app/public/manifest.json | 15 + packages/app/public/robots.txt | 2 + packages/app/public/safari-pinned-tab.svg | 1 + packages/app/src/App.test.tsx | 26 ++ packages/app/src/App.tsx | 107 +++++ packages/app/src/apis.ts | 19 + packages/app/src/components/Root/LogoFull.tsx | 46 +++ packages/app/src/components/Root/LogoIcon.tsx | 47 +++ packages/app/src/components/Root/Root.tsx | 112 +++++ packages/app/src/components/Root/index.ts | 17 + .../app/src/components/catalog/EntityPage.tsx | 390 ++++++++++++++++++ .../app/src/components/search/SearchPage.tsx | 156 +++++++ packages/app/src/index.tsx | 6 + packages/app/src/setupTests.ts | 1 + packages/backend/.eslintrc.js | 1 + packages/backend/Dockerfile | 34 ++ packages/backend/README.md | 66 +++ packages/backend/package.json | 53 +++ packages/backend/src/index.test.ts | 8 + packages/backend/src/index.ts | 109 +++++ packages/backend/src/plugins/app.ts | 14 + packages/backend/src/plugins/auth.ts | 54 +++ packages/backend/src/plugins/catalog.ts | 14 + packages/backend/src/plugins/proxy.ts | 13 + packages/backend/src/plugins/scaffolder.ts | 20 + packages/backend/src/plugins/search.ts | 66 +++ packages/backend/src/plugins/techdocs.ts | 51 +++ packages/backend/src/types.ts | 23 ++ plugins/README.md | 9 + tsconfig.json | 14 + 59 files changed, 2126 insertions(+) create mode 100644 .dockerignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 README.md create mode 100644 app-config.production.yaml create mode 100644 app-config.yaml create mode 100644 backstage.json create mode 100644 catalog-info.yaml create mode 100644 examples/entities.yaml create mode 100644 examples/org.yaml create mode 100644 examples/template/content/catalog-info.yaml create mode 100644 examples/template/content/index.js create mode 100644 examples/template/content/package.json create mode 100644 examples/template/template.yaml create mode 100644 lerna.json create mode 100644 package.json create mode 100644 packages/README.md create mode 100644 packages/app/.eslintrc.js create mode 100644 packages/app/cypress.json create mode 100644 packages/app/cypress/.eslintrc.json create mode 100644 packages/app/cypress/integration/app.js create mode 100644 packages/app/package.json create mode 100644 packages/app/public/android-chrome-192x192.png create mode 100644 packages/app/public/apple-touch-icon.png create mode 100644 packages/app/public/favicon-16x16.png create mode 100644 packages/app/public/favicon-32x32.png create mode 100644 packages/app/public/favicon.ico create mode 100644 packages/app/public/index.html create mode 100644 packages/app/public/manifest.json create mode 100644 packages/app/public/robots.txt create mode 100644 packages/app/public/safari-pinned-tab.svg create mode 100644 packages/app/src/App.test.tsx create mode 100644 packages/app/src/App.tsx create mode 100644 packages/app/src/apis.ts create mode 100644 packages/app/src/components/Root/LogoFull.tsx create mode 100644 packages/app/src/components/Root/LogoIcon.tsx create mode 100644 packages/app/src/components/Root/Root.tsx create mode 100644 packages/app/src/components/Root/index.ts create mode 100644 packages/app/src/components/catalog/EntityPage.tsx create mode 100644 packages/app/src/components/search/SearchPage.tsx create mode 100644 packages/app/src/index.tsx create mode 100644 packages/app/src/setupTests.ts create mode 100644 packages/backend/.eslintrc.js create mode 100644 packages/backend/Dockerfile create mode 100644 packages/backend/README.md create mode 100644 packages/backend/package.json create mode 100644 packages/backend/src/index.test.ts create mode 100644 packages/backend/src/index.ts create mode 100644 packages/backend/src/plugins/app.ts create mode 100644 packages/backend/src/plugins/auth.ts create mode 100644 packages/backend/src/plugins/catalog.ts create mode 100644 packages/backend/src/plugins/proxy.ts create mode 100644 packages/backend/src/plugins/scaffolder.ts create mode 100644 packages/backend/src/plugins/search.ts create mode 100644 packages/backend/src/plugins/techdocs.ts create mode 100644 packages/backend/src/types.ts create mode 100644 plugins/README.md create mode 100644 tsconfig.json 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 0000000000000000000000000000000000000000..eec0ae25b971cae8eb0033c9af7e0f676d1df663 GIT binary patch literal 13599 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 EANz~b2mk;8 literal 0 HcmV?d00001 diff --git a/packages/app/public/apple-touch-icon.png b/packages/app/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3158830ac778a62ff8f08da0e9eeee6e8ada8bfc GIT binary patch literal 12619 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 VSn|oqbSlsa22WQ%mvv4FO#qym7y1AI literal 0 HcmV?d00001 diff --git a/packages/app/public/favicon-32x32.png b/packages/app/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..c0915ece75949f3d917134f55193949927edc633 GIT binary patch literal 1686 zcmd5*YdF&j82@WYD9ojqT$Wo{SSXitp^eRqxy{@++t}P`atlKxQX0)YMm6_KR%p4F z$}J+6LMeH2OXWDHjylI#AJ2#L{k+fbeSgn;`91HK_szmP+3%H7kpcicc}5$z1N!Px-7om}akWbt233fdJ10ExN)z&Z~ATcQ$c2>@ad0I)&=0IMPZ zfJBuET&w|L$8)@+J4!Sl|Nk(stgJlvmnlJ&;dZvl8tO{Q%IJ*5_~q$1+xa={mdc0(T`t!w?RZl>^IxA*%bGu3bF$<*nzTrF> zoQ&9oiO|s=ORAp|1maSVle9kTR(cJ300u{&4iq#dewwjMjZ;umbia|8`fV}#)u6uZ z2{{=Vn6Y8_#3SbXb1ic-&s#nMteerJ5NT@9WJFf_0Z{rXvToRrSOV|5nT6}g)=)7clCVKtHJ1*VPs-V;@XTM)*hF8A>dvO zHk*Y?X8JaA;})mFp7;7TRRuk~8$R*Snh{9txf3X8a4%y!TuP;MHGAGHB;T*cTs()( zPBU^uyOrcpI-AkwlQc~ccBy9xRRz@U7IH^D@piFSO__BF#e(AN-^z2yOmV!NrfGJV z)K=@)T;PiA16X29aqmP@px{GT+&pJ*OCU>i#GE8dw3RuCdu^ zY|d<%y|L(_@zJ9#_m z=Lc$*iilj{bjSDeNH|lRP>yLE)Yq?l!XVsP?uHhR(xnf>5p}vuAuF-j7*tA0oTAH zhFP&ZPMN7z8!K6i|K8u7|AYJvRll0p!ov{KSr|$njU-`o&7fVZN5@WQ|g`Mlirnap=oY~b#iCdHeScp|7sct zd}FL!1NSuuCnVle?ygaP975V!)pzN#*|A{{1* z$fS~)G%HFBO+IIv4>f5HNGY_6SF`0>Gf0kWIFv^nU=X CgrE5U literal 0 HcmV?d00001 diff --git a/packages/app/public/favicon.ico b/packages/app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5e45e5dfbde6f39603d5be60d933c1af14dffb1e GIT binary patch literal 15086 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 D+pS%} literal 0 HcmV?d00001 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": "." + } +}