diff --git a/.changeset/backstage-changelog.js b/.changeset/backstage-changelog.js
new file mode 100644
index 0000000000..99c25b80e1
--- /dev/null
+++ b/.changeset/backstage-changelog.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const {
+ default: defaultChangelogFunctions,
+} = require('@changesets/cli/changelog');
+
+// Custom CHANGELOG generation for changesets, stolen from here with one minor change:
+// https://github.com/atlassian/changesets/blob/main/packages/cli/src/changelog/index.ts
+async function getDependencyReleaseLine(changesets, dependenciesUpdated) {
+ if (dependenciesUpdated.length === 0) return '';
+
+ const updatedDepenenciesList = dependenciesUpdated.map(
+ dependency => ` - ${dependency.name}@${dependency.newVersion}`,
+ );
+
+ // Return one `Updated dependencies` bullet instead of repeating for each changeset; this
+ // sacrifices the commit shas for brevity.
+ return ['- Updated dependencies', ...updatedDepenenciesList].join('\n');
+}
+
+module.exports = {
+ getReleaseLine: defaultChangelogFunctions.getReleaseLine,
+ getDependencyReleaseLine,
+};
diff --git a/.changeset/config.json b/.changeset/config.json
index 44a8523265..86963b7d09 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@1.3.0/schema.json",
- "changelog": "@changesets/cli/changelog",
+ "changelog": "./backstage-changelog.js",
"commit": false,
"linked": [["*"]],
"access": "public",
diff --git a/.changeset/cuddly-donuts-whisper.md b/.changeset/cuddly-donuts-whisper.md
new file mode 100644
index 0000000000..fd1b665b99
--- /dev/null
+++ b/.changeset/cuddly-donuts-whisper.md
@@ -0,0 +1,54 @@
+---
+'@backstage/plugin-explore': patch
+---
+
+Refactors the explore plugin to be more customizable. This includes the following non-breaking changes:
+
+- Introduce new `ExploreLayout` page which can be used to create a custom `ExplorePage`
+- Refactor `ExplorePage` to use a new `ExploreLayout` component
+- Exports existing `DomainExplorerContent`, `GroupsExplorerContent`, & `ToolExplorerContent` components
+- Allows `title` props to be customized
+
+Create a custom explore page in `packages/app/src/components/explore/ExplorePage.tsx`.
+
+```tsx
+import {
+ DomainExplorerContent,
+ ExploreLayout,
+} from '@backstage/plugin-explore';
+import React from 'react';
+import { InnserSourceExplorerContent } from './InnserSourceExplorerContent';
+
+export const ExplorePage = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export const explorePage = ;
+```
+
+Now register the new explore page in `packages/app/src/App.tsx`.
+
+```diff
++ import { explorePage } from './components/explore/ExplorePage';
+
+const routes = (
+
+- } />
++ }>
++ {explorePage}
++
+
+);
+```
diff --git a/.changeset/few-windows-whisper.md b/.changeset/few-windows-whisper.md
new file mode 100644
index 0000000000..565e8798a1
--- /dev/null
+++ b/.changeset/few-windows-whisper.md
@@ -0,0 +1,8 @@
+---
+'@backstage/core-app-api': patch
+'@backstage/core-plugin-api': patch
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-scaffolder': patch
+---
+
+Adding `FeatureFlag` component and treating `FeatureFlags` as first class citizens to composability API
diff --git a/.changeset/funny-toys-talk.md b/.changeset/funny-toys-talk.md
deleted file mode 100644
index 6e6a779ab8..0000000000
--- a/.changeset/funny-toys-talk.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/codemods': patch
----
-
-Fix execution of `jscodeshift` on windows.
diff --git a/.changeset/good-jars-turn.md b/.changeset/good-jars-turn.md
new file mode 100644
index 0000000000..0bbe344ad2
--- /dev/null
+++ b/.changeset/good-jars-turn.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Use the `identityApi` to forward authorization headers to the `search-backend`
diff --git a/.changeset/metal-badgers-carry.md b/.changeset/metal-badgers-carry.md
new file mode 100644
index 0000000000..b82e638c6f
--- /dev/null
+++ b/.changeset/metal-badgers-carry.md
@@ -0,0 +1,25 @@
+---
+'@backstage/plugin-catalog-backend': patch
+'@backstage/plugin-catalog-backend-module-msgraph': patch
+---
+
+Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend`
+to `@backstage/plugin-catalog-backend-module-msgraph`.
+
+The `MicrosoftGraphOrgReaderProcessor` isn't registered by default anymore, if
+you want to continue using it you have to register it manually at the catalog
+builder:
+
+1. Add dependency to `@backstage/plugin-catalog-backend-module-msgraph` to the `package.json` of your backend.
+2. Add the processor to the catalog builder:
+
+```typescript
+// packages/backend/src/plugins/catalog.ts
+builder.addProcessor(
+ MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
+ logger,
+ }),
+);
+```
+
+For more configuration details, see the [README of the `@backstage/plugin-catalog-backend-module-msgraph` package](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md).
diff --git a/.changeset/odd-humans-exercise.md b/.changeset/odd-humans-exercise.md
new file mode 100644
index 0000000000..136ec86b69
--- /dev/null
+++ b/.changeset/odd-humans-exercise.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Make the `create-github-app` command disable webhooks by default.
diff --git a/.changeset/perfect-gifts-compete.md b/.changeset/perfect-gifts-compete.md
new file mode 100644
index 0000000000..7c03798888
--- /dev/null
+++ b/.changeset/perfect-gifts-compete.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Don't export the `defaultGoogleAuthProvider`
diff --git a/.changeset/popular-rice-wonder.md b/.changeset/popular-rice-wonder.md
new file mode 100644
index 0000000000..f815ad949a
--- /dev/null
+++ b/.changeset/popular-rice-wonder.md
@@ -0,0 +1,8 @@
+---
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-catalog-backend': patch
+'@backstage/plugin-scaffolder': patch
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Moved installation instructions from the main [backstage.io](https://backstage.io) documentation to the package README file. These instructions are not generally needed, since the plugin comes installed by default with `npx @backstage/create-app`.
diff --git a/.changeset/purple-papayas-exist.md b/.changeset/purple-papayas-exist.md
deleted file mode 100644
index 4c77558476..0000000000
--- a/.changeset/purple-papayas-exist.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-user-settings': patch
----
-
-Fix a bug that prevented changing themes on the user settings page when the theme `id` didn't match exactly the theme `variant`.
diff --git a/.changeset/rich-trees-chew.md b/.changeset/rich-trees-chew.md
new file mode 100644
index 0000000000..5ec79c36d0
--- /dev/null
+++ b/.changeset/rich-trees-chew.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+chore: bump `@spotify/eslint-config-typescript` from 9.0.0 to 10.0.0
diff --git a/.changeset/seven-adults-act.md b/.changeset/seven-adults-act.md
new file mode 100644
index 0000000000..fa2839aad2
--- /dev/null
+++ b/.changeset/seven-adults-act.md
@@ -0,0 +1,25 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Adds support for custom sign-in resolvers and profile transformations for the
+Google auth provider.
+
+Adds an `ent` claim in Backstage tokens, with a list of
+[entity references](https://backstage.io/docs/features/software-catalog/references)
+related to your signed-in user's identities and groups across multiple systems.
+
+Adds an optional `providerFactories` argument to the `createRouter` exported by
+the `auth-backend` plugin.
+
+Updates `BackstageIdentity` so that
+
+- `idToken` is deprecated in favor of `token`
+- An optional `entity` field is added which represents the entity that the user is represented by within Backstage.
+
+More information:
+
+- [The identity resolver documentation](https://backstage.io/docs/auth/identity-resolver)
+ explains the concepts and shows how to implement your own.
+- The [From Identity to Ownership](https://github.com/backstage/backstage/issues/4089)
+ RFC contains details about how this affects ownership in the catalog
diff --git a/.changeset/silent-ways-laugh.md b/.changeset/silent-ways-laugh.md
new file mode 100644
index 0000000000..ea152c68ef
--- /dev/null
+++ b/.changeset/silent-ways-laugh.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog-backend-module-msgraph': patch
+---
+
+Allow customizations of `MicrosoftGraphOrgReaderProcessor` by passing an
+optional `groupTransformer`, `userTransformer`, and `organizationTransformer`.
diff --git a/.changeset/techdocs-cool-rivers-suffer.md b/.changeset/techdocs-cool-rivers-suffer.md
new file mode 100644
index 0000000000..2386d27525
--- /dev/null
+++ b/.changeset/techdocs-cool-rivers-suffer.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Refactor the implicit logic from `` into an explicit state machine. This resolves some state synchronization issues when content is refreshed or rebuilt in the backend.
diff --git a/.changeset/techdocs-poor-forks-repeat.md b/.changeset/techdocs-poor-forks-repeat.md
new file mode 100644
index 0000000000..89c4e0dc5a
--- /dev/null
+++ b/.changeset/techdocs-poor-forks-repeat.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs-backend': patch
+---
+
+Return a `304 Not Modified` from the `/sync/:namespace/:kind/:name` endpoint if nothing was built. This enables the caller to know whether a refresh of the docs page will return updated content (-> `201 Created`) or not (-> `304 Not Modified`).
diff --git a/.eslintrc.js b/.eslintrc.js
index 4dfcf5317f..c8108f7289 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -25,6 +25,12 @@ module.exports = {
{
// eslint-disable-next-line no-restricted-syntax
templateFile: path.resolve(__dirname, './scripts/copyright-header.txt'),
+ templateVars: {
+ NAME: 'The Backstage Authors',
+ },
+ varRegexps: {
+ NAME: /(The Backstage Authors)|(Spotify AB)/,
+ },
onNonMatchingHeader: 'replace',
},
],
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 206c1a7883..fda500ed02 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,7 +4,7 @@
# The last matching pattern takes precedence.
# https://help.github.com/articles/about-codeowners/
-* @backstage/maintainers
+* @backstage/reviewers
/docs/features/techdocs @backstage/techdocs-core
/docs/features/search @backstage/techdocs-core
/docs/assets/search @backstage/techdocs-core
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 4ff766e5e2..78b2a9b75f 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -149,6 +149,7 @@ Mkdocs
monorepo
Monorepo
monorepos
+msgraph
msw
mysql
namespace
diff --git a/ADOPTERS.md b/ADOPTERS.md
index 31f384e4e6..4abab25e30 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -30,3 +30,4 @@
| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. |
| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. |
| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. |
+| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑🚀 |
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
index 0ab8d8390d..936c27c7cf 100644
--- a/GOVERNANCE.md
+++ b/GOVERNANCE.md
@@ -36,9 +36,33 @@ To become a maintainer you need to demonstrate the following:
If a maintainer is no longer interested or cannot perform the maintainer duties listed above, they should volunteer to be moved to emeritus status. In extreme cases this can also occur by a vote of the sponsors and maintainers per the voting process below.
+# Reviewers
+
+The project also contains a team called [@backstage/reviewers](https://github.com/orgs/backstage/teams/reviewers). This is the team of people who are the fallback in [`CODEOWNERS`](./.github/CODEOWNERS). This team will typically contain the maintainers, and a small number of additional people who are permitted to approve and merge pull requests. The purpose of this group is to offload some of the review work from the maintainers, simplifying and speeding up the review process for contributors.
+
+This responsibility is distinct from the maintainer role. A reviewer must not approve and merge changes that have a level of impact that a maintainer should oversee; see below for clarification. For that class of changes, a reviewer can still review the pull request thoroughly without approving it (e.g. with a comment on the pull request), and is expected to notify `@backstage/maintainers` for final approval. Note that it is best to not use the GitHub review approve functionality for this, since that would let Hall of Fame members self-merge the pull request before maintainers get the chance to look at it.
+
+The following is a non-exhaustive list of types of change, for which a reviewer should defer final decision and merge to a maintainer:
+
+- A larger refactoring that significantly affects the structure of/between packages
+- Changes that settle or alter the trajectory of contested ongoing topics in issues or elsewhere
+- Changes that affect the [Architecture Decision Records](./docs/architecture-decisions)
+- Changes to APIs that have large customer impact, such as the core APIs in `@backstage/core-*` packages, or significant `@backstage/cli` changes.
+- Pull requests whose build checks are not passing fully
+- Additions and removals of entire packages
+- Releases (e.g. pull requests titled `Version Packages`)
+
+A maintainer may suggest an addition to the reviewers team by opening a pull request that modifies [`OWNERS.md`](./OWNERS.md) accordingly. Prospective reviewers are not expected to do this themselves, but should rather ask a maintainer to sponsor their addition. All of the maintainers and sponsors are called to vote on the addition (see the section below about voting). If the vote passes, the pull request can be approved and merged, and the corresponding addition to the GitHub team can be made.
+
+A reviewer can elect to remove themselves from the reviewers group by opening, or asking a maintainer to open, a pull request that modifies [`OWNERS.md`](./OWNERS.md) accordingly. A maintainer will approve and merge the pull request, and the corresponding removal from the GitHub team can be made.
+
+A maintainer can call on the other maintainers and sponsors for a vote to remove a reviewer (see the section below about conflict resolution and voting). If the vote passes, a maintainer creates a pull request that modifies [`OWNERS.md`](./OWNERS.md) accordingly. After approval by another maintainer, the pull request can be merged, and the corresponding removal from the GitHub team can be made.
+
# Conflict resolution and voting
-In general, we prefer that technical issues and maintainer membership are amicably worked out between the persons involved. If a dispute cannot be decided independently, the sponsors and maintainers can be called in to decide an issue. If the sponsors and maintainers themselves cannot decide an issue, the issue will be resolved by voting. The voting process is a simple majority in which each sponsor receives two votes and each maintainer receives one vote.
+In general, we prefer that technical issues and membership are amicably worked out between the persons involved. If a dispute cannot be decided independently, the sponsors and maintainers can be called in to decide an issue. If the sponsors and maintainers themselves cannot decide an issue, the issue will be resolved by voting.
+
+In all cases in this document where voting is mentioned, the voting process is a simple majority in which each sponsor receives two votes and each maintainer receives one vote. If such a majority is reached, the vote is said to have _passed_.
# Adding new projects to the Backstage GitHub organization
diff --git a/OWNERS.md b/OWNERS.md
index cdbc234183..6615046540 100644
--- a/OWNERS.md
+++ b/OWNERS.md
@@ -16,6 +16,18 @@ This page lists all active sponsors and maintainers.
- Ben Lambert ([benjdlambert](https://github.com/benjdlambert)) (Discord: @blam)
- Johan Haals ([jhaals](https://github.com/jhaals)) (Discord: @jhaals)
+# Reviewers
+
+See [`GOVERNANCE.md`](./GOVERNANCE.md) for details about how the reviewers team
+works.
+
+- Patrik Oldsberg ([rugvip](https://github.com/rugvip)) (Discord: @Rugvip)
+- Fredrik Adelöw ([freben](https://github.com/freben)) (Discord: @freben)
+- Ben Lambert ([benjdlambert](https://github.com/benjdlambert)) (Discord: @blam)
+- Johan Haals ([jhaals](https://github.com/jhaals)) (Discord: @jhaals)
+- Himanshu Mishra ([OrkoHunter](https://github.com/OrkoHunter)) (Discord: @OrkoHunter)
+- Tim Hansen ([timbonicus](https://github.com/timbonicus)) (Discord: @timbonicus)
+
# Emeritus maintainers
- Stefan Ålund ([stefanalund](https://github.com/stefanalund)) (Discord: @stalund)
diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md
new file mode 100644
index 0000000000..814ff63729
--- /dev/null
+++ b/docs/auth/identity-resolver.md
@@ -0,0 +1,150 @@
+---
+id: identity-resolver
+title: Identity resolver
+description: Identity resolvers of Backstage users after they sign-in
+---
+
+This guide explains how the identity of a Backstage user is stored inside their
+Backstage Identity Token and how you can customize the Sign-In resolvers to
+include identity and group membership information of the user from other
+external systems. This ultimately helps with determining the ownership of a
+Backstage entity by a user. The ideas here were originally proposed in the RFC
+[#4089](https://github.com/backstage/backstage/issues/4089).
+
+When a user signs in to Backstage, inside the `claims` field of their Backstage
+Token (which are standard JWT tokens) a special `ent` claim is set. `ent`
+contains a list of
+[entity references](../features/software-catalog/references.md), each of which
+denotes an identity or a membership that is relevant to the user. There is no
+guarantee that these correspond to actual existing catalog entities.
+
+Let's take an example sign-in resolver for the Google auth provider and explore
+how the `ent` field inside `claims` can be set.
+
+Inside your `packages/backend/src/plugins/auth.ts` file, you can provide custom
+sign-in resolvers and set them for any of the Authentication providers inside
+`providerFactories` of the `createRouter` imported from the
+`@backstage/plugin-auth-backend` plugin.
+
+```ts
+export default async function createPlugin({
+ ...
+}: PluginEnvironment): Promise {
+ return await createRouter({
+ ...
+ providerFactories: {
+ google: createGoogleProvider({
+ signIn: {
+ resolver: async ({ profile: { email } }, ctx) => {
+ // Call a custom validator function that checks that the email is
+ // valid and on our own company's domain, and throws an Error if it
+ // isn't
+ validateEmail(email);
+
+ // List of entity references that denote the identity and
+ // membership of the user
+ const ent = [];
+
+ // Let's use the username in the email ID as the user's default
+ // unique identifier inside Backstage.
+ const [id] = email.split('@');
+ ent.push(`User:default/${id}`)
+
+ // Let's call the internal LDAP provider to get a list of groups
+ // that the user belongs to, and add those to the list as well
+ const ldapGroups = await getLdapGroups(email);
+ ldapGroups.forEach(group => ent.push(`Group:default/${group}`))
+
+ // Issue the token containing the entity claims
+ const token = await ctx.tokenIssuer.issueToken({
+ claims: { sub: id, ent },
+ });
+ return { id, token };
+ },
+ },
+ }),
+ },
+ });
+}
+```
+
+As you can see, the generated Backstage Token now contains all the claims about
+the identity and membership of the user. Once the sign-in process is complete,
+and we need to find out if a user owns an Entity in the Software Catalog, these
+`ent` claims can be used to determine the ownership.
+
+According to the RFC, the definition of the ownership of an entity E, for a user
+U, is as follows:
+
+- Get all the `ownedBy` relations of E, and call them O
+- Get all the claims of the user U and call them C
+- If any C matches any O, return `true`
+- Get all Group entities that U is a member of, using the regular
+ `memberOf`/`hasMember` relation mechanism, and call them G
+- If any G matches any O, return `true`
+- Otherwise, return `false`
+
+## Default sign-in resolvers
+
+Of course you don't have to customize the sign-in resolver if you don't need to.
+The Auth backend plugin comes with a set of default sign-in resolvers which you
+can use. For example - the Google provider has a default email-based sign-in
+resolver, which will search the catalog for a single user entity that has a
+matching `google.com/email` annotation.
+
+It can be enabled like this
+
+```tsx
+// File: packages/backend/src/plugins/auth.ts
+import { googleEmailSignInResolver } from '@backstage/plugin-auth-backend';
+
+export default async function createPlugin({
+ ...
+}: PluginEnvironment): Promise {
+ return await createRouter({
+ ...
+ providerFactories: {
+ google: createGoogleProvider({
+ signIn: {
+ resolver: googleEmailSignInResolver
+ }
+...
+```
+
+## AuthHandler
+
+Similar to a custom sign-in resolver, you can also write a custom auth handler
+function which is used to verify and convert the auth response into the profile
+that will be presented to the user. This is where you can customize things like
+display name and profile picture.
+
+This is also the place where you can do authorization and validation of the user
+and throw errors if the user should not be allowed access in Backstage.
+
+```tsx
+// File: packages/backend/src/plugins/auth.ts
+export default async function createPlugin({
+ ...
+}: PluginEnvironment): Promise {
+ return await createRouter({
+ ...
+ providerFactories: {
+ google: createGoogleProvider({
+ authHandler: async ({
+ fullProfile // Type: passport.Profile,
+ idToken // Type: (Optional) string,
+ }) => {
+ // Custom validation code goes here
+ return {
+ profile: {
+ email,
+ picture,
+ displayName,
+ }
+ };
+ }
+ })
+ }
+ })
+}
+```
diff --git a/docs/conf/defining.md b/docs/conf/defining.md
index 34b9b11977..f13beb9f77 100644
--- a/docs/conf/defining.md
+++ b/docs/conf/defining.md
@@ -93,6 +93,21 @@ declare the visibility of a leaf node of `type: "string"`.
| `backend` | (Default) Only in backend |
| `secret` | Only in backend and may be excluded from logs for security reasons |
+You can set visibility with an `@visibility` comment in the `Config` Typescript
+interface.
+
+```ts
+export interface Config {
+ app: {
+ /**
+ * Frontend root URL
+ * @visibility frontend
+ */
+ baseUrl: string;
+ };
+}
+```
+
## Validation
Schemas can be validated using the `backstage-cli config:check` command. If you
diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md
index 70541b85db..2189b7d799 100644
--- a/docs/features/software-catalog/index.md
+++ b/docs/features/software-catalog/index.md
@@ -34,9 +34,8 @@ More specifically, the Service Catalog enables two main use-cases:
## Getting Started
The Software Catalog is available to browse at `/catalog`. If you've followed
-[Installing in your Backstage App](./installation.md) in your separate App or
-[Getting Started with Backstage](../../getting-started) for this repo, you
-should be able to browse the catalog at `http://localhost:3000`.
+[Getting Started with Backstage](../../getting-started), you should be able to
+browse the catalog at `http://localhost:3000`.

diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md
deleted file mode 100644
index 0b622fb093..0000000000
--- a/docs/features/software-catalog/installation.md
+++ /dev/null
@@ -1,177 +0,0 @@
----
-id: installation
-title: Installing in your Backstage App
-description: Documentation on How to install Backstage Plugin
----
-
-The catalog plugin comes in two packages, `@backstage/plugin-catalog` and
-`@backstage/plugin-catalog-backend`. Each has their own installation steps,
-outlined below.
-
-## Installing @backstage/plugin-catalog
-
-> **Note that if you used `npx @backstage/create-app`, the plugin is already
-> installed and you can skip to
-> [adding entries to the catalog](#adding-entries-to-the-catalog)**
-
-The catalog frontend plugin should be installed in your `app` package, which is
-created as a part of `@backstage/create-app`. To install the package, run:
-
-```bash
-# From your Backstage root directory
-cd packages/app
-yarn add @backstage/plugin-catalog
-```
-
-### Adding the Plugin to your `packages/app`
-
-Add the two pages that the catalog plugin provides to your app. You can choose
-any name for these routes, but we recommend the following:
-
-```tsx
-// packages/app/src/App.tsx
-import {
- catalogPlugin,
- CatalogIndexPage,
- CatalogEntityPage,
-} from '@backstage/plugin-catalog';
-
-// Add to the top-level routes, directly within
-} />
-}>
- {/*
- This is the root of the custom entity pages for your app, refer to the example app
- in the main repo or the output of @backstage/create-app for an example
- */}
-
-
-```
-
-The catalog plugin also has one external route that needs to be bound for it to
-function: the `createComponent` route which should link to the page where the
-user can create components. In a typical setup the create component route will
-be linked to the Scaffolder plugin's template index page:
-
-```ts
-// packages/app/src/App.tsx
-import { catalogPlugin } from '@backstage/plugin-catalog';
-import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
-
-const app = createApp({
- // ...
- bindRoutes({ bind }) {
- bind(catalogPlugin.externalRoutes, {
- createComponent: scaffolderPlugin.routes.root,
- });
- },
-});
-```
-
-You may also want to add a link to the catalog index page to your sidebar:
-
-```tsx
-// packages/app/src/components/Root.tsx
-import HomeIcon from '@material-ui/icons/Home';
-
-// Somewhere within the
-;
-```
-
-This is all that is needed for the frontend part of the Catalog plugin to work!
-
-## Gotchas that we will fix
-
-Since the catalog plugin currently ships with a sentry plugin `InfoCard`
-installed by default, you'll need to set `sentry.organization` in your
-`app-config.yaml`. For example:
-
-```yaml
-sentry:
- organization: Acme Corporation
-```
-
-If you've created an app with an older version of `@backstage/create-app` or
-`@backstage/cli create-app`, be sure to remove the Welcome plugin from the app,
-as that will conflict with the catalog routes.
-
-## Installing @backstage/plugin-catalog-backend
-
-> **Note that if you used `npx @backstage/create-app`, the plugin is already
-> installed and you can skip to
-> [adding entries to the catalog](#adding-entries-to-the-catalog)**
-
-The catalog backend should be installed in your `backend` package, which is
-created as a part of `@backstage/create-app`. To install the package, run:
-
-```bash
-# From your Backstage root directory
-cd packages/backend
-yarn add @backstage/plugin-catalog-backend
-```
-
-### Adding the Plugin to your `packages/backend`
-
-You'll need to add the plugin to the `backend`'s router. You can do this by
-creating a file called `packages/backend/src/plugins/catalog.ts` with contents
-matching
-[catalog.ts in the create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts).
-
-Once the `catalog.ts` router setup file is in place, add the router to
-`packages/backend/src/index.ts`:
-
-```ts
-import catalog from './plugins/catalog';
-
-const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
-
-const apiRouter = Router();
-/** several different routers */
-apiRouter.use('/catalog', await catalog(catalogEnv));
-```
-
-### Adding Entries to the Catalog
-
-At this point the catalog backend is installed in your backend package, but you
-will not have any entities loaded.
-
-To get up and running and try out some templates quickly, you can add some of
-our example templates through static configuration. Add the following to the
-`catalog.locations` section in your `app-config.yaml`:
-
-```yaml
-catalog:
- locations:
- # Backstage Example Components
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/artist-lookup-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-order-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/podcast-api-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/queue-proxy-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/searcher-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-lib-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/www-artist-component.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/shuffle-api-component.yaml
-```
-
-### Running the Backend
-
-Finally, start up Backstage with the new configuration:
-
-```bash
-# Run from the root to start both backend and frontend
-yarn dev
-
-# Alternatively, run only the backend from its own package
-cd packages/backend
-yarn start
-```
-
-If you've also set up the frontend plugin, you should be ready to go browse the
-catalog at [localhost:3000](http://localhost:3000) now!
diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md
new file mode 100644
index 0000000000..cdd8166889
--- /dev/null
+++ b/docs/features/software-templates/configuration.md
@@ -0,0 +1,52 @@
+---
+id: configuration
+title: Software Template Configuration
+sidebar_label: Configuration
+description: Configuration options for Backstage Software Templates
+---
+
+Backstage software templates create source code, so your Backstage application
+needs to be set up to allow repository creation.
+
+This is done in your `app-config.yaml` by adding
+[Backstage integrations](https://backstage.io/docs/integrations/) for the
+appropriate source code repository for your organization.
+
+> Note: Integrations may already be set up as part of your `app-config.yaml`.
+
+The next step is to add
+[add templates](http://backstage.io/docs/features/software-templates/adding-templates)
+to your Backstage app.
+
+### GitHub
+
+For GitHub, you can configure who can see the new repositories that are created
+by specifying `visibility` option. Valid options are `public`, `private` and
+`internal`. The `internal` option is for GitHub Enterprise clients, which means
+public within the enterprise.
+
+```yaml
+scaffolder:
+ github:
+ visibility: public # or 'internal' or 'private'
+```
+
+### Disabling Docker in Docker situation (Optional)
+
+Software Templates use
+[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as a templating
+library. By default it will use the
+[scaffolder-backend/Cookiecutter](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile)
+docker image.
+
+If you are running Backstage from a Docker container and you want to avoid
+calling a container inside a container, you can set up Cookiecutter in your own
+image, this will use the local installation instead.
+
+You can do so by including the following lines in the last step of your
+`Dockerfile`:
+
+```Dockerfile
+RUN apt-get update && apt-get install -y python3 python3-pip
+RUN pip3 install cookiecutter
+```
diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md
index 12ce6e3ed3..1434d62a29 100644
--- a/docs/features/software-templates/index.md
+++ b/docs/features/software-templates/index.md
@@ -17,10 +17,8 @@ locations like GitHub or GitLab.
### Getting Started
-> Be sure to have covered [Installing in your Backstage App](./installation.md)
-> for your separate App or
-> [Getting Started with Backstage](../../getting-started) for this repo before
-> proceeding.
+> Be sure to have covered
+> [Getting Started with Backstage](../../getting-started) before proceeding.
The Software Templates are available under `/create`. For local development you
should be able to reach them at `http://localhost:3000/create`.
diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md
deleted file mode 100644
index d5643ca955..0000000000
--- a/docs/features/software-templates/installation.md
+++ /dev/null
@@ -1,280 +0,0 @@
----
-id: installation
-title: Installing in your Backstage App
-description: Documentation on How to install Backstage App
----
-
-The scaffolder plugin comes in two packages, `@backstage/plugin-scaffolder` and
-`@backstage/plugin-scaffolder-backend`. Each has their own installation steps,
-outlined below.
-
-The Scaffolder plugin also depends on the Software Catalog. Instructions for how
-to set that up can be found [here](../software-catalog/installation.md).
-
-## Installing @backstage/plugin-scaffolder
-
-> **Note that if you used `npx @backstage/create-app`, the plugin may already be
-> present**
-
-The scaffolder frontend plugin should be installed in your `app` package, which
-is created as a part of `@backstage/create-app`. To install the package, run:
-
-```bash
-# From your Backstage root directory
-cd packages/app
-yarn add @backstage/plugin-scaffolder
-```
-
-### Adding the Plugin to your `packages/app`
-
-Add the root page that the Scaffolder plugin provides to your app. You can
-choose any path for the route, but we recommend the following:
-
-```tsx
-import { ScaffolderPage } from '@backstage/plugin-scaffolder';
-
-// Add to the top-level routes, directly within
-} />;
-```
-
-You may also want to add a link to the template index page to your sidebar:
-
-```tsx
-import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
-
-// Somewhere within the
-;
-```
-
-This is all that is needed for the frontend part of the Scaffolder plugin to
-work!
-
-## Installing @backstage/plugin-scaffolder-backend
-
-> **Note that if you used `npx @backstage/create-app`, the plugin may already be
-> present**
-
-The scaffolder backend should be installed in your `backend` package, which is
-created as a part of `@backstage/create-app`. To install the package, run:
-
-```bash
-# From your Backstage root directory
-cd packages/backend
-yarn add @backstage/plugin-scaffolder-backend
-```
-
-### Adding the Plugin to your `packages/backend`
-
-You'll need to add the plugin to the `backend`'s router. You can do this by
-creating a file called `packages/backend/src/plugins/scaffolder.ts` with the
-following contents to get you up and running quickly.
-
-```ts
-import {
- DockerContainerRunner,
- SingleHostDiscovery,
-} from '@backstage/backend-common';
-import {
- CookieCutter,
- createRouter,
- Preparers,
- Publishers,
- CreateReactAppTemplater,
- Templaters,
-} from '@backstage/plugin-scaffolder-backend';
-import type { PluginEnvironment } from '../types';
-import Docker from 'dockerode';
-import { CatalogClient } from '@backstage/catalog-client';
-
-export default async function createPlugin({
- logger,
- config,
- database,
- reader,
-}: PluginEnvironment) {
- const dockerClient = new Docker();
- const containerRunner = new DockerContainerRunner({ dockerClient });
-
- const cookiecutterTemplater = new CookieCutter({ containerRunner });
- const craTemplater = new CreateReactAppTemplater({ containerRunner });
- const templaters = new Templaters();
-
- templaters.register('cookiecutter', cookiecutterTemplater);
- templaters.register('cra', craTemplater);
-
- const preparers = await Preparers.fromConfig(config, { logger });
- const publishers = await Publishers.fromConfig(config, { logger });
-
- const discovery = SingleHostDiscovery.fromConfig(config);
- const catalogClient = new CatalogClient({ discoveryApi: discovery });
-
- return await createRouter({
- preparers,
- templaters,
- publishers,
- logger,
- config,
- database,
- catalogClient,
- reader,
- });
-}
-```
-
-Once the `scaffolder.ts` router setup file is in place, add the router to
-`packages/backend/src/index.ts`:
-
-```ts
-import scaffolder from './plugins/scaffolder';
-
-const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
-
-const apiRouter = Router();
-/* several router .use calls */
-
-/* add this line */
-apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
-```
-
-### Adding Templates
-
-At this point the scaffolder backend is installed in your backend package, but
-you will not have any templates available to use. These need to be added to the
-software catalog, as they are represented as entities of kind
-[Template](../software-catalog/descriptor-format.md#kind-template). You can find
-out more about adding templates [here](./adding-templates.md).
-
-To get up and running and try out some templates quickly, you can add some of
-our example templates through static configuration. Add the following to the
-`catalog.locations` section in your `app-config.yaml`:
-
-```yaml
-catalog:
- locations:
- # Backstage Example Templates
- - type: url
- target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
- - type: url
- target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
- - type: url
- target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
-```
-
-### Runtime Dependencies / Configuration
-
-For the scaffolder backend plugin to function, you'll need to setup the
-integrations config in your `app-config.yaml`.
-
-You can find help for different providers below.
-
-> Note: Some of this configuration may already be set up as part of your
-> `app-config.yaml`. We're moving away from the duplicated config for
-> authentication in the `scaffolder` section and using `integrations` instead.
-
-#### GitHub
-
-The GitHub access token is retrieved from environment variables via the config.
-The config file needs to specify what environment variable the token is
-retrieved from. Your config should have the following objects.
-
-You can configure who can see the new repositories that the scaffolder creates
-by specifying `visibility` option. Valid options are `public`, `private` and
-`internal`. The `internal` option is for GitHub Enterprise clients, which means
-public within the enterprise.
-
-```yaml
-integrations:
- github:
- - host: github.com
- token: ${GITHUB_TOKEN}
-
-scaffolder:
- github:
- visibility: public # or 'internal' or 'private'
-```
-
-#### GitLab
-
-For GitLab, we currently support the configuration of the GitLab publisher and
-allows to configure the private access token and the base URL of a GitLab
-instance:
-
-```yaml
-integrations:
- gitlab:
- - host: gitlab.com
- token: ${GITLAB_TOKEN}
-```
-
-#### Bitbucket
-
-For Bitbucket there are two authentication methods supported. Either `token` or
-a combination of `appPassword` and `username`. It looks like either of the
-following:
-
-```yaml
-integrations:
- bitbucket:
- - host: bitbucket.org
- token: ${BITBUCKET_TOKEN}
-```
-
-or
-
-```yaml
-integrations:
- bitbucket:
- - host: bitbucket.org
- appPassword: ${BITBUCKET_APP_PASSWORD}
- username: ${BITBUCKET_USERNAME}
-```
-
-#### Azure DevOps
-
-For Azure DevOps we support both the preparer and publisher stage with the
-configuration of a private access token (PAT). For the publisher it's also
-required to define the base URL for the client to connect to the service. This
-will hopefully support on-prem installations as well but that has not been
-verified.
-
-```yaml
-integrations:
- azure:
- - host: dev.azure.com
- token: ${AZURE_TOKEN}
-```
-
-### Running the Backend
-
-Finally, make sure you have a local Docker daemon running, and start up the
-backend with the new configuration:
-
-```bash
-cd packages/backend
-GITHUB_TOKEN= yarn start
-```
-
-If you've also set up the frontend plugin, so you should be ready to go browse
-the templates at [localhost:3000/create](http://localhost:3000/create) now!
-
-### Disabling Docker in Docker situation (Optional)
-
-Software Templates use
-[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as templating
-library. By default it will use the
-[spotify/backstage-cookiecutter](https://github.com/backstage/backstage/blob/37e35b910afc7d1270855aed0ec4718aba366c91/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile)
-docker image.
-
-If you are running Backstage from a Docker container and you want to avoid
-calling a container inside a container, you can set up Cookiecutter in your own
-image, this will use the local installation instead.
-
-You can do so by including the following lines in the last step of your
-`Dockerfile`:
-
-```Dockerfile
-RUN apt-get update && apt-get install -y python3 python3-pip
-RUN pip3 install cookiecutter
-```
diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md
index b9340330a8..3fdc942ac4 100644
--- a/docs/features/techdocs/creating-and-publishing.md
+++ b/docs/features/techdocs/creating-and-publishing.md
@@ -28,10 +28,10 @@ scratch.
### Use the documentation template
Your working Backstage instance should by default have a documentation template
-added. If not, follow these
-[instructions](../software-templates/installation.md#adding-templates) to add
-the documentation template. The template creates a component with only TechDocs
-configuration and default markdown files as below mentioned in manual
+added. If not, copy the catalog locations from the
+[create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs)
+to add the documentation template. The template creates a component with only
+TechDocs configuration and default markdown files as below mentioned in manual
documentation setup, and is otherwise empty.

diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md
index 62ee4867c8..119629915e 100644
--- a/docs/features/techdocs/using-cloud-storage.md
+++ b/docs/features/techdocs/using-cloud-storage.md
@@ -370,7 +370,7 @@ techdocs:
openStackSwift:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
- userName: ${OPENSTACK_SWIFT_STORAGE_USERNAME}
+ username: ${OPENSTACK_SWIFT_STORAGE_USERNAME}
password: ${OPENSTACK_SWIFT_STORAGE_PASSWORD}
authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL}
keystoneAuthVersion: ${OPENSTACK_SWIFT_STORAGE_AUTH_VERSION}
diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md
index 163674f00a..3b3e3dd5ed 100644
--- a/docs/integrations/azure/locations.md
+++ b/docs/integrations/azure/locations.md
@@ -6,8 +6,8 @@ sidebar_label: Locations
description: Integrating source code stored in Azure DevOps into the Backstage catalog
---
-The Azure integration supports loading catalog entities from Azure DevOps.
-Entities can be added to
+The Azure DevOps integration supports loading catalog entities from Azure
+DevOps. Entities can be added to
[static catalog configuration](../../features/software-catalog/configuration.md),
or registered with the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md
new file mode 100644
index 0000000000..c359960f4d
--- /dev/null
+++ b/docs/integrations/azure/org.md
@@ -0,0 +1,14 @@
+---
+id: org
+title: Microsoft Azure Active Directory Organizational Data
+sidebar_label: Org Data
+# prettier-ignore
+description: Importing users and groups from a Microsoft Azure Active Directory into Backstage
+---
+
+The Backstage catalog can be set up to ingest organizational data - users and
+teams - directly from an tenant in Microsoft Azure Active Directory via the
+Microsoft Graph API.
+
+More details on this are available in the
+[README of the `@backstage/plugin-catalog-backend-module-msgraph` package](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md).
diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md
index 4480cd69e3..1d728b76aa 100644
--- a/docs/plugins/backend-plugin.md
+++ b/docs/plugins/backend-plugin.md
@@ -99,7 +99,7 @@ import carmen from './plugins/carmen';
async function main() {
// ...
const carmenEnv = useHotMemoize(module, () => createEnv('carmen'));
- apiRouter.use('/carmen', await carmen(badgesEnv));
+ apiRouter.use('/carmen', await carmen(carmenEnv));
```
After you start the backend (e.g. using `yarn start-backend` from the repo
@@ -111,3 +111,42 @@ curl localhost:7000/api/carmen/health
```
This should return `{"status":"ok"}` like before. Success!
+
+## Making Use of a Database
+
+The Backstage backend comes with a builtin facility for SQL database access.
+Most plugins that have persistence needs will choose to make use of this
+facility, so that Backstage operators can manage database needs uniformly.
+
+As part of the environment object that is passed to your `createPlugin`
+function, there is a `database` field. You can use that to get a
+[Knex](http://knexjs.org/) connection object.
+
+```ts
+// in packages/backend/src/plugins/carmen.ts
+export default async function createPlugin(env: PluginEnvironment) {
+ const db: Knex = await env.database.getClient();
+
+ // You will then pass this client into your actual plugin implementation
+ // code, maybe similar to the following:
+ const model = new CarmenDatabaseModel(db);
+ return await createRouter({
+ model: model,
+ logger: env.logger,
+ });
+}
+```
+
+You may note that the `getClient` call has no parameters. This is because all
+plugin database needs are configured under the `backend.database` config key of
+your `app-config.yaml`. The framework may even make sure behind the scenes that
+the logical database is created automatically if it doesn't exist, based on
+rules that the Backstage operator decides on.
+
+The framework does not handle database schema migrations for you, however. The
+builtin plugins in the main repo have chosen to use the Knex library to manage
+schema migrations as well, but you can do so in any manner that you see fit.
+
+See the [Knex library documentation](http://knexjs.org/) for examples and
+details on how to write schema migrations and perform SQL queries against your
+database..
diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md
index 87d23b45d5..805e321093 100644
--- a/docs/plugins/github-apps.md
+++ b/docs/plugins/github-apps.md
@@ -43,6 +43,10 @@ root of the project which you can then use as an `include` in your
`app-config.yaml`. You can go ahead and
[skip ahead](#including-in-integrations-config) if you've already got an app.
+Note that the created app will have a webhook that is disabled by default and
+points to `smee.io`, which is intended for local development. There's also
+currently no part of Backstage that makes use of the webhook.
+
### GitHub Enterprise
You have to create the GitHub Application manually using these
@@ -84,3 +88,12 @@ integrations:
apps:
- $include: example-backstage-app-credentials.yaml
```
+
+### Permissions for pull requests
+
+These are the minimum permissions required for creating a pull request with
+Backstage software templates:
+
+- Read and Write permissions for `Contents`.
+- Read and write permissions for `Pull Requests` and `Issues`.
+- Read permissions on `Metadata`.
diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md
new file mode 100644
index 0000000000..27b800b3d2
--- /dev/null
+++ b/docs/tutorials/configuring-plugin-databases.md
@@ -0,0 +1,187 @@
+---
+id: configuring-plugin-databases
+title: Configuring Plugin Databases
+# prettier-ignore
+description: Guide on how to configure Backstage databases.
+---
+
+This guide covers a variety of production persistence use cases which are
+supported out of the box by Backstage. The database manager allows the developer
+to set the client and database connection details on a per plugin basis in
+addition to the base client and connection configuration. This means that you
+can use a SQLite 3 in-memory database for a specific plugin whilst using
+PostgreSQL for everything else and so on.
+
+By default, Backstage uses automatically created databases for each plugin whose
+names follow the `backstage_plugin_` pattern, e.g.
+`backstage_plugin_auth`. You can configure a different database name prefix for
+use cases where you have multiple deployments running on a shared database
+instance or cluster.
+
+With infrastructure defined as code or data (Terraform, AWS CloudFormation,
+etc.), you may have database credentials which lack permissions to create new
+databases or you do not have control over the database names. In these
+instances, you can set the database name and connection information on a per
+plugin basis as mentioned earlier.
+
+Backstage supports all of these use cases with the `DatabaseManager` provided by
+`@backstage/backend-common`. We will now cover how to use and configure
+Backstage's databases.
+
+## Prerequisites
+
+### Dependencies
+
+Please ensure the appropriate database drivers are installed in your `backend`
+package. If you intend to use both `postgres` and `sqlite3`, you can install
+both of them.
+
+```shell
+cd packages/backend
+
+# install pg if you need postgres
+yarn add pg
+
+# install sqlite3 if you intend to set it as the client
+yarn add sqlite3
+```
+
+From an operational perspective, you only need to install drivers for clients
+that are actively used.
+
+### Database Manager
+
+Existing Backstage instances should be updated to use `DatabaseManager` from
+`@backstage/backend-common` in your `packages/backend/src/index.ts` file, the
+`SingleConnectionDatabaseManager` has been deprecated. Import the manager and
+update the references as shown below if this is not the case:
+
+```diff
+import {
+- SingleConnectionDatabaseManager,
++ DatabaseManager,
+} from '@backstage/backend-common';
+
+// ...
+
+function makeCreateEnv(config: Config) {
+ // ...
+- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
++ const databaseManager = DatabaseManager.fromConfig(config);
+ // ...
+}
+```
+
+## Configuration
+
+You should set the base database client and connection information in your
+`app-config.yaml` (or equivalent) file. The base client and configuration is
+used as the default which is extended for each plugin with the same or unset
+client type. If a client type is specified for a specific plugin which does not
+match the base client, the configuration set for the plugin will be used as is
+without extending the base configuration.
+
+Client type and configuration for plugins need to be defined under
+**`backend.database.plugin.`**. As an example, `catalog` is the
+`pluginId` for the catalog plugin and any configuration defined under that block
+is specific to that plugin. We will now explore more detailed example
+configurations below.
+
+### Minimal In-Memory Configuration
+
+In the example below, we are using `sqlite3` in-memory databases for all
+plugins. You may want to use this configuration for testing or other non-durable
+use cases.
+
+```yaml
+backend:
+ database:
+ client: sqlite3
+ connection: ':memory:'
+```
+
+### PostgreSQL
+
+The example below uses PostgreSQL (`pg`) as the database client for all plugins.
+The `auth` plugin uses a user defined database name instead of the automatically
+generated one which would have been `backstage_plugin_auth`.
+
+```yaml
+backend:
+ database:
+ client: pg
+ connection:
+ host: some.example-pg-instance.tld
+ user: postgres
+ password: password
+ port: 5432
+ plugin:
+ auth:
+ connection:
+ database: pg_auth_set_by_user
+```
+
+### Custom Database Name Prefix
+
+The configuration below uses `example_prefix_` as the database name prefix
+instead of `backstage_plugin_`. Plugins such as `auth` and `catalog` will use
+databases named `example_prefix_auth` and `example_prefix_catalog` respectively.
+
+```yaml
+backend:
+ database:
+ client: pg
+ connection:
+ host: some.example-pg-instance.tld
+ user: postgres
+ password: password
+ port: 5432
+ prefix: 'example_prefix_'
+```
+
+### Connection Configuration Per Plugin
+
+Both `auth` and `catalog` use connection configuration with different
+credentials and database names. This type of configuration can be useful for
+environments with infrastructure as code or data which may provide randomly
+generated credentials and/or database names.
+
+```yaml
+backend:
+ database:
+ client: pg
+ connection: 'postgresql://some.example-pg-instance.tld:5432'
+ plugin:
+ auth:
+ connection: 'postgresql://fort:knox@some.example-pg-instance.tld:5432/unwitting_fox_jumps'
+ catalog:
+ connection: 'postgresql://bank:reserve@some.example-pg-instance.tld:5432/shuffle_ransack_playback'
+```
+
+### PostgreSQL and SQLite 3
+
+The example below uses PostgreSQL (`pg`) as the database client for all plugins
+except the `auth` plugin which uses `sqlite3`. As the `auth` plugin's client
+type is different from the base client type, the connection configuration for
+`auth` is used verbatim without extending the base configuration for PostgreSQL.
+
+```yaml
+backend:
+ database:
+ client: pg
+ connection: 'postgresql://foo:bar@some.example-pg-instance.tld:5432'
+ plugin:
+ auth:
+ client: sqlite3
+ connection: ':memory:'
+```
+
+## Check Your Databases
+
+The `DatabaseManager` will attempt to create the databases if they do not exist.
+If you have set credentials per plugin because the credentials in the base
+configuration do not have permissions to create databases, you must ensure they
+exist before starting the service. The service will not be able to create them,
+it can only use them.
+
+Good luck!
diff --git a/microsite/data/plugins/gke-usage.yaml b/microsite/data/plugins/gke-usage.yaml
new file mode 100644
index 0000000000..42d3e3a287
--- /dev/null
+++ b/microsite/data/plugins/gke-usage.yaml
@@ -0,0 +1,15 @@
+---
+title: GKE Usage
+author: BESTSELLER
+authorUrl: bestsellerit.com
+category: Discovery
+description: This plugin will show you the cost and resource usage of your application within GKE.
+documentation: https://github.com/BESTSELLER/backstage-plugin-gkeusage/blob/master/README.md
+iconUrl: https://bestsellerit.com/img/google-container-engine_avatar.svg
+npmPackageName: '@bestsellerit/backstage-plugin-gkeusage'
+tags:
+ - gke
+ - cost
+ - google
+ - usage
+ - metering
diff --git a/microsite/package.json b/microsite/package.json
index 3112bbfe17..1894e37d84 100644
--- a/microsite/package.json
+++ b/microsite/package.json
@@ -20,7 +20,7 @@
"docusaurus": "^2.0.0-alpha.70",
"js-yaml": "^4.1.0",
"prettier": "^2.3.1",
- "yarn-lock-check": "^1.0.4"
+ "yarn-lock-check": "^1.0.5"
},
"prettier": "@spotify/prettier-config"
}
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index f2ce425307..ea1e1a4e89 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -33,7 +33,6 @@
"label": "Software Catalog",
"ids": [
"features/software-catalog/software-catalog-overview",
- "features/software-catalog/installation",
"features/software-catalog/configuration",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
@@ -62,7 +61,7 @@
"label": "Software Templates",
"ids": [
"features/software-templates/software-templates-index",
- "features/software-templates/installation",
+ "features/software-templates/configuration",
"features/software-templates/adding-templates",
"features/software-templates/writing-templates",
"features/software-templates/builtin-actions",
@@ -103,8 +102,8 @@
"integrations/index",
{
"type": "subcategory",
- "label": "Azure DevOps",
- "ids": ["integrations/azure/locations"]
+ "label": "Azure",
+ "ids": ["integrations/azure/locations", "integrations/azure/org"]
},
{
"type": "subcategory",
@@ -206,6 +205,7 @@
},
"auth/add-auth-provider",
"auth/using-auth",
+ "auth/identity-resolver",
"auth/auth-backend",
"auth/oauth",
"auth/auth-backend-classes",
diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js
index 5d61e55bc1..ea78fb462a 100644
--- a/microsite/siteConfig.js
+++ b/microsite/siteConfig.js
@@ -92,10 +92,8 @@ const siteConfig = {
cleanUrl: true,
// Open Graph and Twitter card images.
- ogImage:
- 'logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png',
- twitterImage:
- 'logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png',
+ ogImage: 'img/sharing-opengraph.png',
+ twitterImage: 'img/twitter-summary.png',
// For sites with a sizable amount of content, set collapsible to true.
// Expand/collapse the links and subcategories under categories.
diff --git a/microsite/static/img/cortex.png b/microsite/static/img/cortex.png
index 6a45d0ca06..d16b3f9c32 100644
Binary files a/microsite/static/img/cortex.png and b/microsite/static/img/cortex.png differ
diff --git a/microsite/static/img/sharing-opengraph.png b/microsite/static/img/sharing-opengraph.png
new file mode 100644
index 0000000000..7d304f81ed
Binary files /dev/null and b/microsite/static/img/sharing-opengraph.png differ
diff --git a/microsite/static/img/twitter-summary.png b/microsite/static/img/twitter-summary.png
new file mode 100644
index 0000000000..7330b071ee
Binary files /dev/null and b/microsite/static/img/twitter-summary.png differ
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 6f71394265..a89235bf98 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -9,13 +9,6 @@
dependencies:
"@babel/highlight" "^7.0.0"
-"@babel/code-frame@^7.0.0":
- version "7.12.13"
- resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
- integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
- dependencies:
- "@babel/highlight" "^7.12.13"
-
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11":
version "7.12.11"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@@ -227,11 +220,6 @@
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
-"@babel/helper-validator-identifier@^7.14.0":
- version "7.14.0"
- resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288"
- integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==
-
"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11":
version "7.12.11"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f"
@@ -265,15 +253,6 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/highlight@^7.12.13":
- version "7.14.0"
- resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf"
- integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==
- dependencies:
- "@babel/helper-validator-identifier" "^7.14.0"
- chalk "^2.0.0"
- js-tokens "^4.0.0"
-
"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7":
version "7.12.11"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
@@ -942,29 +921,11 @@
dependencies:
"@types/node" "*"
-"@types/glob@^7.1.3":
- version "7.1.3"
- resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
- integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
- dependencies:
- "@types/minimatch" "*"
- "@types/node" "*"
-
-"@types/minimatch@*":
- version "3.0.4"
- resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
- integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
-
"@types/node@*":
version "14.14.20"
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340"
integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==
-"@types/node@^15.6.1":
- version "15.9.0"
- resolved "https://registry.npmjs.org/@types/node/-/node-15.9.0.tgz#0b7f6c33ca5618fe329a9d832b478b4964d325a8"
- integrity sha512-AR1Vq1Ei1GaA5FjKL5PBqblTZsL5M+monvGSZwe6sSIdGiuu7Xr/pNwWJY+0ZQuN8AapD/XMB5IzBAyYRFbocA==
-
"@types/q@^1.5.1":
version "1.5.4"
resolved "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
@@ -1449,11 +1410,6 @@ buffer@^5.2.1:
base64-js "^1.3.1"
ieee754 "^1.1.13"
-builtin-modules@^1.1.1:
- version "1.1.1"
- resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
- integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
-
bytes@1:
version "1.0.0"
resolved "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8"
@@ -1567,7 +1523,7 @@ caw@^2.0.0, caw@^2.0.1:
tunnel-agent "^0.6.0"
url-to-options "^1.0.1"
-chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -1758,7 +1714,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
-commander@^2.12.1, commander@^2.8.1:
+commander@^2.8.1:
version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -2230,11 +2186,6 @@ diacritics-map@^0.1.0:
resolved "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz#6dfc0ff9d01000a2edf2865371cac316e94977af"
integrity sha1-bfwP+dAQAKLt8oZTccrDFulJd68=
-diff@^4.0.1:
- version "4.0.2"
- resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
- integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
-
dir-glob@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
@@ -3091,19 +3042,7 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
-glob@^7.0.0, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6, glob@~7.1.1:
- version "7.1.6"
- resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
- integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-glob@^7.1.1, glob@^7.1.7:
+glob@^7.0.0, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6, glob@^7.1.7, glob@~7.1.1:
version "7.1.7"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
@@ -3638,13 +3577,6 @@ is-core-module@^2.1.0:
dependencies:
has "^1.0.3"
-is-core-module@^2.2.0:
- version "2.4.0"
- resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1"
- integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==
- dependencies:
- has "^1.0.3"
-
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -4457,7 +4389,7 @@ mixin-deep@^1.1.3, mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
-mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
+mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -5681,14 +5613,6 @@ resolve@^1.1.6, resolve@^1.10.0:
is-core-module "^2.1.0"
path-parse "^1.0.6"
-resolve@^1.3.2:
- version "1.20.0"
- resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
- integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
- dependencies:
- is-core-module "^2.2.0"
- path-parse "^1.0.6"
-
responselike@1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
@@ -6437,37 +6361,11 @@ truncate-html@^1.0.3:
"@types/cheerio" "^0.22.8"
cheerio "0.22.0"
-tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
+tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslint@^6.1.3:
- version "6.1.3"
- resolved "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"
- integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==
- dependencies:
- "@babel/code-frame" "^7.0.0"
- builtin-modules "^1.1.1"
- chalk "^2.3.0"
- commander "^2.12.1"
- diff "^4.0.1"
- glob "^7.1.1"
- js-yaml "^3.13.1"
- minimatch "^3.0.4"
- mkdirp "^0.5.3"
- resolve "^1.3.2"
- semver "^5.3.0"
- tslib "^1.13.0"
- tsutils "^2.29.0"
-
-tsutils@^2.29.0:
- version "2.29.0"
- resolved "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
- integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
- dependencies:
- tslib "^1.8.1"
-
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -6493,11 +6391,6 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@^4.3.2:
- version "4.3.2"
- resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
- integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
-
unbzip2-stream@^1.0.9:
version "1.4.3"
resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
@@ -6767,18 +6660,14 @@ yargs@^2.3.0:
dependencies:
wordwrap "0.0.2"
-yarn-lock-check@^1.0.4:
- version "1.0.4"
- resolved "https://registry.npmjs.org/yarn-lock-check/-/yarn-lock-check-1.0.4.tgz#a0373de051be0c8442d8933070df7a45595263b4"
- integrity sha512-Gj0wRN85c4OPZUlE7WsQ0a1COv38uyeWWR0YAvJr2Vxw1f32bwK19xySjUZlxt9o4QopJkd8g6x6CLv81OHwYg==
+yarn-lock-check@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/yarn-lock-check/-/yarn-lock-check-1.0.5.tgz#69d9516385f3ff010d0e2b0e87fbbd9bb1ffecaf"
+ integrity sha512-dxmV4LpIBrRAPbPg+klyGvqdVo3Y6PgJs6ERJYXf0HSEst7klDmvKKqQUtk+wrIRCoMDIVIzSUTZsuC4zr/tFQ==
dependencies:
- "@types/glob" "^7.1.3"
- "@types/node" "^15.6.1"
"@yarnpkg/lockfile" "^1.1.0"
glob "^7.1.7"
ini "^2.0.0"
- tslint "^6.1.3"
- typescript "^4.3.2"
yauzl@^2.4.2:
version "2.10.0"
diff --git a/mkdocs.yml b/mkdocs.yml
index 67b97b6ffb..6c4af107ce 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -30,7 +30,6 @@ nav:
- Core Features:
- Software Catalog:
- Overview: 'features/software-catalog/index.md'
- - Installing in your Backstage App: 'features/software-catalog/installation.md'
- Catalog Configuration: 'features/software-catalog/configuration.md'
- System Model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
@@ -49,7 +48,7 @@ nav:
- Troubleshooting: 'features/kubernetes/troubleshooting.md'
- Software Templates:
- Overview: 'features/software-templates/index.md'
- - Installing in your Backstage App: 'features/software-templates/installation.md'
+ - Configuration: 'features/software-templates/configuration.md'
- Adding your own Templates: 'features/software-templates/adding-templates.md'
- Writing Templates: 'features/software-templates/writing-templates.md'
- Builtin Actions: 'features/software-templates/builtin-actions.md'
@@ -76,8 +75,9 @@ nav:
- FAQ: 'features/techdocs/FAQ.md'
- Integrations:
- Overview: 'integrations/index.md'
- - Azure DevOps:
+ - Azure:
- Locations: 'integrations/azure/locations.md'
+ - Org Data: 'integrations/azure/org.md'
- Bitbucket:
- Locations: 'integrations/bitbucket/locations.md'
- Discovery: 'integrations/bitbucket/discovery.md'
@@ -133,6 +133,7 @@ nav:
- OneLogin: 'auth/onelogin/provider.md'
- Adding authentication providers: 'auth/add-auth-provider.md'
- Using authentication and identity: 'auth/using-auth.md'
+ - Sign in resolvers: 'auth/identity-resolver.md'
- Auth backend: 'auth/auth-backend.md'
- OAuth and OpenID Connect: 'auth/oauth.md'
- Auth backend classes: 'auth/auth-backend-classes.md'
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index c200104775..f6d69f050c 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,23 @@
# example-app
+## 0.2.33
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@0.2.3
+ - @backstage/plugin-catalog@0.6.3
+ - @backstage/cli@0.7.1
+ - @backstage/plugin-api-docs@0.5.0
+ - @backstage/plugin-jenkins@0.4.5
+ - @backstage/plugin-techdocs@0.9.6
+ - @backstage/plugin-circleci@0.2.16
+ - @backstage/plugin-catalog-import@0.5.10
+ - @backstage/plugin-sentry@0.3.12
+ - @backstage/plugin-user-settings@0.2.11
+ - @backstage/catalog-model@0.8.3
+ - @backstage/core@0.7.13
+
## 0.2.32
### Patch Changes
diff --git a/packages/app/package.json b/packages/app/package.json
index 1b0b3aa22c..9d22f6bf28 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,19 +1,21 @@
{
"name": "example-app",
- "version": "0.2.32",
+ "version": "0.2.33",
"private": true,
"bundled": true,
"dependencies": {
- "@backstage/catalog-model": "^0.8.2",
- "@backstage/cli": "^0.7.0",
- "@backstage/core": "^0.7.12",
+ "@backstage/catalog-model": "^0.8.3",
+ "@backstage/cli": "^0.7.1",
+ "@backstage/core": "^0.7.13",
"@backstage/integration-react": "^0.1.3",
- "@backstage/plugin-api-docs": "^0.4.15",
+ "@backstage/core-app-api": "^0.1.2",
+ "@backstage/core-components": "^0.1.1",
+ "@backstage/plugin-api-docs": "^0.5.0",
"@backstage/plugin-badges": "^0.2.2",
- "@backstage/plugin-catalog": "^0.6.2",
- "@backstage/plugin-catalog-import": "^0.5.9",
- "@backstage/plugin-catalog-react": "^0.2.2",
- "@backstage/plugin-circleci": "^0.2.15",
+ "@backstage/plugin-catalog": "^0.6.3",
+ "@backstage/plugin-catalog-import": "^0.5.10",
+ "@backstage/plugin-catalog-react": "^0.2.3",
+ "@backstage/plugin-circleci": "^0.2.16",
"@backstage/plugin-cloudbuild": "^0.2.16",
"@backstage/plugin-code-coverage": "^0.1.4",
"@backstage/plugin-cost-insights": "^0.10.2",
@@ -21,7 +23,7 @@
"@backstage/plugin-gcp-projects": "^0.2.6",
"@backstage/plugin-github-actions": "^0.4.9",
"@backstage/plugin-graphiql": "^0.2.11",
- "@backstage/plugin-jenkins": "^0.4.4",
+ "@backstage/plugin-jenkins": "^0.4.5",
"@backstage/plugin-kafka": "^0.2.8",
"@backstage/plugin-kubernetes": "^0.4.5",
"@backstage/plugin-lighthouse": "^0.2.17",
@@ -31,12 +33,12 @@
"@backstage/plugin-rollbar": "^0.3.6",
"@backstage/plugin-scaffolder": "^0.9.8",
"@backstage/plugin-search": "^0.4.0",
- "@backstage/plugin-sentry": "^0.3.11",
+ "@backstage/plugin-sentry": "^0.3.12",
"@backstage/plugin-shortcuts": "^0.1.2",
"@backstage/plugin-tech-radar": "^0.4.0",
- "@backstage/plugin-techdocs": "^0.9.5",
+ "@backstage/plugin-techdocs": "^0.9.6",
"@backstage/plugin-todo": "^0.1.2",
- "@backstage/plugin-user-settings": "^0.2.10",
+ "@backstage/plugin-user-settings": "^0.2.11",
"@backstage/theme": "^0.2.8",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index b14f9a3163..cc65cd65b3 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -14,13 +14,12 @@
* limitations under the License.
*/
+import { createApp, FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
- createApp,
- FlatRoutes,
OAuthRequestDialog,
SignInPage,
-} from '@backstage/core';
+} from '@backstage/core-components';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import {
CatalogEntityPage,
@@ -65,6 +64,7 @@ const app = createApp({
// Custom icon example
alert: AlarmIcon,
},
+
components: {
SignInPage: props => {
return (
@@ -116,6 +116,7 @@ const routes = (
/>
} />
} />
+
} />
} />
} />
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index 6256697036..c37137f524 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,59 @@
# @backstage/backend-common
+## 0.8.3
+
+### Patch Changes
+
+- e5cdf0560: Provide a more clear error message when database connection fails.
+- 772dbdb51: Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database
+ connection manager, `DatabaseManager`, which allows developers to configure database
+ connections on a per plugin basis.
+
+ The `backend.database` config path allows you to set `prefix` to use an
+ alternate prefix for automatically generated database names, the default is
+ `backstage_plugin_`. Use `backend.database.plugin.` to set plugin
+ specific database connection configuration, e.g.
+
+ ```yaml
+ backend:
+ database:
+ client: 'pg',
+ prefix: 'custom_prefix_'
+ connection:
+ host: 'localhost'
+ user: 'foo'
+ password: 'bar'
+ plugin:
+ catalog:
+ connection:
+ database: 'database_name_overriden'
+ scaffolder:
+ client: 'sqlite3'
+ connection: ':memory:'
+ ```
+
+ Migrate existing backstage installations by swapping out the database manager in the
+ `packages/backend/src/index.ts` file as shown below:
+
+ ```diff
+ import {
+ - SingleConnectionDatabaseManager,
+ + DatabaseManager,
+ } from '@backstage/backend-common';
+
+ // ...
+
+ function makeCreateEnv(config: Config) {
+ // ...
+ - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
+ + const databaseManager = DatabaseManager.fromConfig(config);
+ // ...
+ }
+ ```
+
+- Updated dependencies
+ - @backstage/config-loader@0.6.4
+
## 0.8.2
### Patch Changes
diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md
index 068220cad3..fcddb8dda5 100644
--- a/packages/backend-common/api-report.md
+++ b/packages/backend-common/api-report.md
@@ -100,6 +100,12 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl;
// @public (undocumented)
export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise;
+// @public (undocumented)
+export class DatabaseManager {
+ forPlugin(pluginId: string): PluginDatabaseManager;
+ static fromConfig(config: Config): DatabaseManager;
+ }
+
// @public (undocumented)
export class DockerContainerRunner implements ContainerRunner {
constructor({ dockerClient }: {
@@ -326,11 +332,8 @@ export type ServiceBuilder = {
// @public (undocumented)
export function setRootLogger(newLogger: winston.Logger): void;
-// @public
-export class SingleConnectionDatabaseManager {
- forPlugin(pluginId: string): PluginDatabaseManager;
- static fromConfig(config: Config): SingleConnectionDatabaseManager;
- }
+// @public @deprecated
+export const SingleConnectionDatabaseManager: typeof DatabaseManager;
// @public
export class SingleHostDiscovery implements PluginEndpointDiscovery {
diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts
index 875660a594..b42ce3eca5 100644
--- a/packages/backend-common/config.d.ts
+++ b/packages/backend-common/config.d.ts
@@ -53,20 +53,30 @@ export interface Config {
};
};
- /** Database connection configuration, select database type using the `client` field */
- database:
- | {
- client: 'sqlite3';
- connection: ':memory:' | string | { filename: string };
- }
- | {
- client: 'pg';
+ /** Database connection configuration, select base database type using the `client` field */
+ database: {
+ /** Default database client to use */
+ client: 'sqlite3' | 'pg';
+ /**
+ * Base database connection string or Knex object
+ * @secret
+ */
+ connection: string | object;
+ /** Database name prefix override */
+ prefix?: string;
+ /** Plugin specific database configuration and client override */
+ plugin?: {
+ [pluginId: string]: {
+ /** Database client override */
+ client?: 'sqlite3' | 'pg';
/**
- * PostgreSQL connection string or knex configuration object.
+ * Database connection string or Knex object override
* @secret
*/
- connection: string | object;
+ connection?: string | object;
};
+ };
+ };
/** Cache connection configuration, select cache type using the `store` field */
cache?:
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index fc57ee59a8..04231a6a69 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.8.2",
+ "version": "0.8.3",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,7 +31,7 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.5",
- "@backstage/config-loader": "^0.6.2",
+ "@backstage/config-loader": "^0.6.4",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.6",
"@google-cloud/storage": "^5.8.0",
@@ -76,7 +76,7 @@
}
},
"devDependencies": {
- "@backstage/cli": "^0.7.0",
+ "@backstage/cli": "^0.7.1",
"@backstage/test-utils": "^0.1.12",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts
new file mode 100644
index 0000000000..8f3331403d
--- /dev/null
+++ b/packages/backend-common/src/database/DatabaseManager.test.ts
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ConfigReader } from '@backstage/config';
+import { omit } from 'lodash';
+import { createDatabaseClient, ensureDatabaseExists } from './connection';
+import { DatabaseManager } from './DatabaseManager';
+
+jest.mock('./connection', () => ({
+ ...jest.requireActual('./connection'),
+ createDatabaseClient: jest.fn(),
+ ensureDatabaseExists: jest.fn(),
+}));
+
+describe('DatabaseManager', () => {
+ // This is similar to the ts-jest `mocked` helper.
+ const mocked = (f: Function) => f as jest.Mock;
+
+ afterEach(() => jest.resetAllMocks());
+
+ describe('DatabaseManager.fromConfig', () => {
+ it('accesses the backend.database key', () => {
+ const config = new ConfigReader({
+ backend: {
+ database: {
+ client: 'pg',
+ connection: {
+ host: 'localhost',
+ user: 'foo',
+ password: 'bar',
+ database: 'foodb',
+ },
+ },
+ },
+ });
+ const getConfigSpy = jest.spyOn(config, 'getConfig');
+ DatabaseManager.fromConfig(config);
+
+ expect(getConfigSpy).toHaveBeenCalledWith('backend.database');
+ });
+ });
+
+ describe('DatabaseManager.forPlugin', () => {
+ const config = {
+ backend: {
+ database: {
+ client: 'pg',
+ prefix: 'test_prefix_',
+ connection: {
+ host: 'localhost',
+ user: 'foo',
+ password: 'bar',
+ database: 'foodb',
+ },
+ plugin: {
+ testdbname: {
+ connection: {
+ database: 'database_name_overriden',
+ },
+ },
+ differentclient: {
+ client: 'sqlite3',
+ connection: {
+ filename: 'plugin_with_different_client',
+ },
+ },
+ differentclientconnstring: {
+ client: 'sqlite3',
+ connection: ':memory:',
+ },
+ stringoverride: {
+ connection: 'postgresql://testuser:testpass@acme:5432/userdbname',
+ },
+ },
+ },
+ },
+ };
+ const manager = DatabaseManager.fromConfig(new ConfigReader(config));
+
+ it('connects to a plugin database using default config', async () => {
+ const pluginId = 'pluginwithoutconfig';
+
+ await manager.forPlugin(pluginId).getClient();
+ expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1);
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [baseConfig, overrides] = mockCalls[0];
+
+ // default config should be passed through to underlying connector
+ expect(baseConfig.get()).toMatchObject({
+ client: 'pg',
+ connection: omit(config.backend.database.connection, ['database']),
+ });
+
+ // override using database name generated from pluginId and prefix
+ expect(overrides).toMatchObject({
+ connection: {
+ database: `${config.backend.database.prefix}${pluginId}`,
+ },
+ });
+ });
+
+ it('provides a plugin db which uses components from top level connection string', async () => {
+ const testManager = DatabaseManager.fromConfig(
+ new ConfigReader({
+ backend: {
+ database: {
+ client: 'pg',
+ connection: 'postgresql://foo:bar@acme:5432/foodb',
+ },
+ },
+ }),
+ );
+
+ await testManager.forPlugin('pluginwithoutconfig').getClient();
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [baseConfig, overrides] = mockCalls[0];
+
+ // parsed connection string **without** db name should be passed through
+ expect(baseConfig.get()).toMatchObject({
+ connection: {
+ host: 'acme',
+ user: 'foo',
+ password: 'bar',
+ port: '5432',
+ },
+ });
+
+ // we expect a pg database name override with ${prefix} followed by pluginId
+ expect(overrides).toHaveProperty(
+ 'connection.database',
+ expect.stringContaining('pluginwithoutconfig'),
+ );
+ });
+
+ it('uses top level sqlite database filename if plugin config is not present', async () => {
+ const testManager = DatabaseManager.fromConfig(
+ new ConfigReader({
+ backend: {
+ database: {
+ client: 'sqlite3',
+ connection: 'some-file-path',
+ },
+ },
+ }),
+ );
+
+ await testManager.forPlugin('pluginwithoutconfig').getClient();
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [_, overrides] = mockCalls[0];
+
+ expect(overrides).toHaveProperty(
+ 'connection.filename',
+ expect.stringContaining('some-file-path'),
+ );
+ });
+
+ it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => {
+ const testManager = DatabaseManager.fromConfig(
+ new ConfigReader({
+ backend: {
+ database: {
+ client: 'sqlite3',
+ connection: ':memory:',
+ },
+ },
+ }),
+ );
+
+ await testManager.forPlugin('pluginwithoutconfig').getClient();
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [_, overrides] = mockCalls[0];
+
+ expect(overrides).toHaveProperty(
+ 'connection.filename',
+ expect.stringContaining(':memory:'),
+ );
+ });
+
+ it('connects to a plugin database using a specific database name', async () => {
+ // testdbname.connection.database is set in config
+ await manager.forPlugin('testdbname').getClient();
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [_baseConfig, overrides] = mockCalls[0];
+
+ // simple case where only database name is overriden
+ expect(overrides).toMatchObject({
+ connection: {
+ database: 'database_name_overriden',
+ },
+ });
+ });
+
+ it('ensure plugin specific database is created', async () => {
+ const pluginId = 'testdbname';
+ // testdbname.connection.database is set in config
+ await manager.forPlugin(pluginId).getClient();
+
+ const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1);
+ const [_, dbname] = mockCalls[0];
+
+ expect(dbname).toEqual(
+ config.backend.database.plugin[pluginId].connection.database,
+ );
+ });
+
+ it('provides different plugins with their own databases', async () => {
+ await manager.forPlugin('plugin1').getClient();
+ await manager.forPlugin('plugin2').getClient();
+
+ expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(2);
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls;
+ const [plugin1CallArgs, plugin2CallArgs] = mockCalls;
+
+ // database name overrides should be different
+ expect(plugin1CallArgs[1].connection.database).not.toEqual(
+ plugin2CallArgs[1].connection.database,
+ );
+ });
+
+ it('uses plugin connection as base if default client is different from plugin client', async () => {
+ const pluginId = 'differentclient';
+ await manager.forPlugin(pluginId).getClient();
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [baseConfig, _overrides] = mockCalls[0];
+
+ // plugin connection should be used as base config, client is different
+ expect(baseConfig.get()).toMatchObject({
+ client: 'sqlite3',
+ connection: config.backend.database.plugin[pluginId].connection,
+ });
+ });
+
+ it('provides database client specific base and override when client set under plugin', async () => {
+ const pluginId = 'differentclient';
+ await manager.forPlugin(pluginId).getClient();
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [baseConfig, overrides] = mockCalls[0];
+
+ // plugin client should be sqlite3
+ expect(baseConfig.get().client).toEqual('sqlite3');
+
+ // sqlite3 uses 'filename' instead of 'database'
+ expect(overrides).toHaveProperty('connection.filename');
+ });
+
+ it('provides database client specific base from plugin connection string when client set under plugin', async () => {
+ const pluginId = 'differentclientconnstring';
+ await manager.forPlugin(pluginId).getClient();
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [baseConfig, overrides] = mockCalls[0];
+
+ expect(baseConfig.get().client).toEqual('sqlite3');
+
+ expect(overrides).toHaveProperty('connection.filename', ':memory:');
+ });
+
+ it('generates a database name override when prefix is not explicitly set', async () => {
+ const testManager = DatabaseManager.fromConfig(
+ new ConfigReader({
+ backend: {
+ database: {
+ client: 'pg',
+ connection: {
+ host: 'localhost',
+ user: 'foo',
+ password: 'bar',
+ database: 'foodb',
+ },
+ },
+ },
+ }),
+ );
+
+ await testManager.forPlugin('testplugin').getClient();
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [_baseConfig, overrides] = mockCalls[0];
+
+ expect(overrides).toHaveProperty(
+ 'connection.database',
+ expect.stringContaining('backstage_plugin_'),
+ );
+ });
+
+ it('uses values from plugin connection string if top level client should be used', async () => {
+ const pluginId = 'stringoverride';
+ await manager.forPlugin(pluginId).getClient();
+
+ const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
+ const [baseConfig, overrides] = mockCalls[0];
+
+ // plugin client should be pg
+ expect(baseConfig.get().client).toEqual('pg');
+
+ expect(overrides).toHaveProperty(
+ 'connection.database',
+ expect.stringContaining('userdbname'),
+ );
+ });
+ });
+});
diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts
new file mode 100644
index 0000000000..9e13f565d3
--- /dev/null
+++ b/packages/backend-common/src/database/DatabaseManager.ts
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Knex } from 'knex';
+import { omit } from 'lodash';
+import { Config, ConfigReader, JsonObject } from '@backstage/config';
+import {
+ createDatabaseClient,
+ ensureDatabaseExists,
+ createNameOverride,
+ normalizeConnection,
+} from './connection';
+import { PluginDatabaseManager } from './types';
+
+/**
+ * Provides a config lookup path for a plugin's config block.
+ */
+function pluginPath(pluginId: string): string {
+ return `plugin.${pluginId}`;
+}
+
+export class DatabaseManager {
+ /**
+ * Creates a DatabaseManager from `backend.database` config.
+ *
+ * The database manager allows the user to set connection and client settings on a per pluginId
+ * basis by defining a database config block under `plugin.` in addition to top level
+ * defaults. Optionally, a user may set `prefix` which is used to prefix generated database
+ * names if config is not provided.
+ *
+ * @param config The loaded application configuration.
+ */
+ static fromConfig(config: Config): DatabaseManager {
+ const databaseConfig = config.getConfig('backend.database');
+
+ return new DatabaseManager(
+ databaseConfig,
+ databaseConfig.getOptionalString('prefix'),
+ );
+ }
+
+ private constructor(
+ private readonly config: Config,
+ private readonly prefix: string = 'backstage_plugin_',
+ ) {}
+
+ /**
+ * Generates a PluginDatabaseManager for consumption by plugins.
+ *
+ * @param pluginId The plugin that the database manager should be created for. Plugin names
+ * should be unique as they are used to look up database config overrides under
+ * `backend.database.plugin`.
+ */
+ forPlugin(pluginId: string): PluginDatabaseManager {
+ const _this = this;
+
+ return {
+ getClient(): Promise {
+ return _this.getDatabase(pluginId);
+ },
+ };
+ }
+
+ /**
+ * Provides the canonical database name for a given plugin.
+ *
+ * This method provides the effective database name which is determined using global
+ * and plugin specific database config. If no explicit database name is configured,
+ * this method will provide a generated name which is the pluginId prefixed with
+ * 'backstage_plugin_'.
+ *
+ * @param pluginId Lookup the database name for given plugin
+ * @returns String representing the plugin's database name
+ */
+ private getDatabaseName(pluginId: string): string {
+ const connection = this.getConnectionConfig(pluginId);
+
+ if (this.getClientType(pluginId).client === 'sqlite3') {
+ // sqlite database name should fallback to ':memory:' as a special case
+ return (
+ (connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:'
+ );
+ }
+ // all other supported databases should fallback to an auto-prefixed name
+ return (
+ (connection as Knex.ConnectionConfig)?.database ??
+ `${this.prefix}${pluginId}`
+ );
+ }
+
+ /**
+ * Provides the client type which should be used for a given plugin.
+ *
+ * The client type is determined by plugin specific config if present. Otherwise the base
+ * client is used as the fallback.
+ *
+ * @param pluginId Plugin to get the client type for
+ * @returns Object with client type returned as `client` and boolean representing whether
+ * or not the client was overridden as `overridden`
+ */
+ private getClientType(
+ pluginId: string,
+ ): {
+ client: string;
+ overridden: boolean;
+ } {
+ const pluginClient = this.config.getOptionalString(
+ `${pluginPath(pluginId)}.client`,
+ );
+
+ const baseClient = this.config.getString('client');
+ const client = pluginClient ?? baseClient;
+ return {
+ client,
+ overridden: client !== baseClient,
+ };
+ }
+
+ /**
+ * Provides a Knex connection plugin config by combining base and plugin config.
+ *
+ * This method provides a baseConfig for a plugin database connector. If the client type
+ * has not been overridden, the global connection config will be included with plugin
+ * specific config as the base. Values from the plugin connection take precedence over the
+ * base. Base database name is omitted for all supported databases excluding SQLite.
+ */
+ private getConnectionConfig(
+ pluginId: string,
+ ): Partial {
+ const { client, overridden } = this.getClientType(pluginId);
+
+ let baseConnection = normalizeConnection(
+ this.config.get('connection'),
+ this.config.getString('client'),
+ );
+ // As databases cannot be shared, the `database` property from the base connection
+ // is omitted. SQLite3's `filename` property is an exception as this is used as a
+ // directory elsewhere so we preserve `filename`.
+ baseConnection = omit(baseConnection, 'database');
+
+ // get and normalize optional plugin specific database connection
+ const connection = normalizeConnection(
+ this.config.getOptional(`${pluginPath(pluginId)}.connection`),
+ client,
+ );
+
+ return {
+ // include base connection if client type has not been overriden
+ ...(overridden ? {} : baseConnection),
+ ...connection,
+ };
+ }
+
+ /**
+ * Provides a Knex database config for a given plugin.
+ *
+ * This method provides a Knex configuration object along with the plugin's client type.
+ *
+ * @param pluginId The plugin that the database config should correspond with
+ */
+ private getConfigForPlugin(pluginId: string): Knex.Config {
+ const { client } = this.getClientType(pluginId);
+
+ return {
+ client,
+ connection: this.getConnectionConfig(pluginId),
+ };
+ }
+
+ /**
+ * Provides a partial Knex.Config database name override for a given plugin.
+ *
+ * @param pluginId Target plugin to get database name override
+ * @returns Partial Knex.Config with database name override
+ */
+ private getDatabaseOverrides(pluginId: string): Knex.Config {
+ return createNameOverride(
+ this.getClientType(pluginId).client,
+ this.getDatabaseName(pluginId),
+ );
+ }
+
+ /**
+ * Provides a scoped Knex client for a plugin as per application config.
+ *
+ * @param pluginId Plugin to get a Knex client for
+ * @returns Promise which resolves to a scoped Knex database client for a plugin
+ */
+ private async getDatabase(pluginId: string): Promise {
+ const pluginConfig = new ConfigReader(
+ this.getConfigForPlugin(pluginId) as JsonObject,
+ );
+
+ const databaseName = this.getDatabaseName(pluginId);
+ try {
+ await ensureDatabaseExists(pluginConfig, databaseName);
+ } catch (error) {
+ throw new Error(
+ `Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
+ );
+ }
+
+ return createDatabaseClient(
+ pluginConfig,
+ this.getDatabaseOverrides(pluginId),
+ );
+ }
+}
diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts
index 3466b0f79d..852dd33944 100644
--- a/packages/backend-common/src/database/SingleConnection.test.ts
+++ b/packages/backend-common/src/database/SingleConnection.test.ts
@@ -15,10 +15,14 @@
*/
import { ConfigReader } from '@backstage/config';
-import { createDatabaseClient } from './connection';
+import { createDatabaseClient, ensureDatabaseExists } from './connection';
import { SingleConnectionDatabaseManager } from './SingleConnection';
-jest.mock('./connection');
+jest.mock('./connection', () => ({
+ ...jest.requireActual('./connection'),
+ createDatabaseClient: jest.fn(),
+ ensureDatabaseExists: jest.fn(),
+}));
describe('SingleConnectionDatabaseManager', () => {
const defaultConfigOptions = {
@@ -43,9 +47,8 @@ describe('SingleConnectionDatabaseManager', () => {
describe('SingleConnectionDatabaseManager.fromConfig', () => {
it('accesses the backend.database key', () => {
- const getConfig = jest.fn();
const config = defaultConfig();
- config.getConfig = getConfig;
+ const getConfig = jest.spyOn(config, 'getConfig');
SingleConnectionDatabaseManager.fromConfig(config);
@@ -64,7 +67,6 @@ describe('SingleConnectionDatabaseManager', () => {
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const callArgs = mockCalls[0];
- expect(callArgs[0].get()).toEqual(defaultConfigOptions.backend.database);
expect(callArgs[1].connection.database).toEqual(
`backstage_plugin_${pluginId}`,
);
@@ -85,5 +87,13 @@ describe('SingleConnectionDatabaseManager', () => {
plugin2CallArgs[1].connection.database,
);
});
+
+ it('ensure plugin database is created', async () => {
+ await manager.forPlugin('test').getClient();
+ const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1);
+ const [_, database] = mockCalls[0];
+
+ expect(database).toEqual('backstage_plugin_test');
+ });
});
});
diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts
index 1c5931a662..5bdd99c0ae 100644
--- a/packages/backend-common/src/database/SingleConnection.ts
+++ b/packages/backend-common/src/database/SingleConnection.ts
@@ -14,69 +14,14 @@
* limitations under the License.
*/
-import { Knex } from 'knex';
-import { Config } from '@backstage/config';
-import { createDatabaseClient, ensureDatabaseExists } from './connection';
-import { PluginDatabaseManager } from './types';
+import { DatabaseManager } from './DatabaseManager';
/**
* Implements a Database Manager which will automatically create new databases
* for plugins when requested. All requested databases are created with the
* credentials provided; if the database already exists no attempt to create
* the database will be made.
+ *
+ * @deprecated Use `DatabaseManager` from `@backend-common` instead.
*/
-export class SingleConnectionDatabaseManager {
- /**
- * Creates a new SingleConnectionDatabaseManager instance by reading from the `backend`
- * config section, specifically the `.database` key for discovering the management
- * database configuration.
- *
- * @param config The loaded application configuration.
- */
- static fromConfig(config: Config): SingleConnectionDatabaseManager {
- return new SingleConnectionDatabaseManager(
- config.getConfig('backend.database'),
- );
- }
-
- private constructor(private readonly config: Config) {}
-
- /**
- * Generates a PluginDatabaseManager for consumption by plugins.
- *
- * @param pluginId The plugin that the database manager should be created for. Plugin names should be unique.
- */
- forPlugin(pluginId: string): PluginDatabaseManager {
- const _this = this;
-
- return {
- getClient(): Promise {
- return _this.getDatabase(pluginId);
- },
- };
- }
-
- private async getDatabase(pluginId: string): Promise {
- const config = this.config;
- const overrides = SingleConnectionDatabaseManager.getDatabaseOverrides(
- pluginId,
- );
- const overrideConfig = overrides.connection as Knex.ConnectionConfig;
- await this.ensureDatabase(overrideConfig.database);
-
- return createDatabaseClient(config, overrides);
- }
-
- private static getDatabaseOverrides(pluginId: string): Knex.Config {
- return {
- connection: {
- database: `backstage_plugin_${pluginId}`,
- },
- };
- }
-
- private async ensureDatabase(database: string) {
- const config = this.config;
- await ensureDatabaseExists(config, database);
- }
-}
+export const SingleConnectionDatabaseManager = DatabaseManager;
diff --git a/packages/backend-common/src/database/connection.test.ts b/packages/backend-common/src/database/connection.test.ts
index 6ab163ff7f..fa7ccd480a 100644
--- a/packages/backend-common/src/database/connection.test.ts
+++ b/packages/backend-common/src/database/connection.test.ts
@@ -15,7 +15,11 @@
*/
import { ConfigReader } from '@backstage/config';
-import { createDatabaseClient } from './connection';
+import {
+ createDatabaseClient,
+ createNameOverride,
+ parseConnectionString,
+} from './connection';
describe('database connection', () => {
describe('createDatabaseClient', () => {
@@ -103,4 +107,49 @@ describe('database connection', () => {
).toThrowError();
});
});
+
+ describe('createNameOverride', () => {
+ it('returns Knex config for postgres', () => {
+ expect(createNameOverride('pg', 'testpg')).toHaveProperty(
+ 'connection.database',
+ 'testpg',
+ );
+ });
+
+ it('returns Knex config for sqlite', () => {
+ expect(createNameOverride('sqlite3', 'testsqlite')).toHaveProperty(
+ 'connection.filename',
+ 'testsqlite',
+ );
+ });
+
+ it('returns Knex config for mysql', () => {
+ expect(createNameOverride('mysql', 'testmysql')).toHaveProperty(
+ 'connection.database',
+ 'testmysql',
+ );
+ });
+
+ it('throws an error for unknown connection', () => {
+ expect(() => createNameOverride('unknown', 'testname')).toThrowError();
+ });
+ });
+
+ describe('parseConnectionString', () => {
+ it('returns parsed Knex.StaticConnectionConfig for postgres', () => {
+ expect(
+ parseConnectionString('postgresql://foo:bar@acme:5432/foodb', 'pg'),
+ ).toHaveProperty('database', 'foodb');
+ });
+
+ it('returns parsed Knex.StaticConnectionConfig for mysql2', () => {
+ expect(
+ parseConnectionString('mysql://foo:bar@acme:3306/foodb', 'mysql2'),
+ ).toHaveProperty('database', 'foodb');
+ });
+
+ it('throws an error if client hint is not provided', () => {
+ expect(() => parseConnectionString('sqlite://')).toThrow();
+ });
+ });
});
diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts
index fb16915cf4..46f040d9d0 100644
--- a/packages/backend-common/src/database/connection.ts
+++ b/packages/backend-common/src/database/connection.ts
@@ -14,14 +14,28 @@
* limitations under the License.
*/
-import { Config } from '@backstage/config';
+import { Config, JsonObject } from '@backstage/config';
+import { InputError } from '@backstage/errors';
import knexFactory, { Knex } from 'knex';
import { mergeDatabaseConfig } from './config';
-import { createMysqlDatabaseClient, ensureMysqlDatabaseExists } from './mysql';
-import { createPgDatabaseClient, ensurePgDatabaseExists } from './postgres';
-import { createSqliteDatabaseClient } from './sqlite3';
+import { DatabaseConnector } from './types';
-type DatabaseClient = 'pg' | 'sqlite3' | string;
+import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors';
+
+type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string;
+
+/**
+ * Mapping of client type to supported database connectors
+ *
+ * Database connectors can be aliased here, for example mysql2 uses
+ * the same connector as mysql.
+ */
+const ConnectorMapping: Record = {
+ pg: pgConnector,
+ sqlite3: sqlite3Connector,
+ mysql: mysqlConnector,
+ mysql2: mysqlConnector,
+};
/**
* Creates a knex database connection
@@ -35,15 +49,10 @@ export function createDatabaseClient(
) {
const client: DatabaseClient = dbConfig.getString('client');
- if (client === 'pg') {
- return createPgDatabaseClient(dbConfig, overrides);
- } else if (client === 'mysql' || client === 'mysql2') {
- return createMysqlDatabaseClient(dbConfig, overrides);
- } else if (client === 'sqlite3') {
- return createSqliteDatabaseClient(dbConfig, overrides);
- }
-
- return knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides));
+ return (
+ ConnectorMapping[client]?.createClient(dbConfig, overrides) ??
+ knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides))
+ );
}
/**
@@ -58,14 +67,66 @@ export const createDatabase = createDatabaseClient;
export async function ensureDatabaseExists(
dbConfig: Config,
...databases: Array
-) {
+): Promise {
const client: DatabaseClient = dbConfig.getString('client');
- if (client === 'pg') {
- return ensurePgDatabaseExists(dbConfig, ...databases);
- } else if (client === 'mysql' || client === 'mysql2') {
- return ensureMysqlDatabaseExists(dbConfig, ...databases);
+ return ConnectorMapping[client]?.ensureDatabaseExists?.(
+ dbConfig,
+ ...databases,
+ );
+}
+
+/**
+ * Provides a Knex.Config object with the provided database name for a given client.
+ */
+export function createNameOverride(
+ client: string,
+ name: string,
+): Partial {
+ try {
+ return ConnectorMapping[client].createNameOverride(name);
+ } catch (e) {
+ throw new InputError(
+ `Unable to create database name override for '${client}' connector`,
+ e,
+ );
+ }
+}
+
+/**
+ * Parses a connection string for a given client and provides a connection config.
+ */
+export function parseConnectionString(
+ connectionString: string,
+ client?: string,
+): Knex.StaticConnectionConfig {
+ if (typeof client === 'undefined' || client === null) {
+ throw new InputError(
+ 'Database connection string client type auto-detection is not yet supported.',
+ );
}
- return undefined;
+ try {
+ return ConnectorMapping[client].parseConnectionString(connectionString);
+ } catch (e) {
+ throw new InputError(
+ `Unable to parse connection string for '${client}' connector`,
+ );
+ }
+}
+
+/**
+ * Normalizes a connection config or string into an object which can be passed to Knex.
+ */
+export function normalizeConnection(
+ connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
+ client: string,
+): Partial {
+ if (typeof connection === 'undefined' || connection === null) {
+ return {};
+ }
+
+ return typeof connection === 'string' || connection instanceof String
+ ? parseConnectionString(connection as string, client)
+ : connection;
}
diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts b/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts
new file mode 100644
index 0000000000..b41736153a
--- /dev/null
+++ b/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import defaultNameOverride from './defaultNameOverride';
+
+describe('defaultNameOverride()', () => {
+ it('returns a partial knex static connection config with database name', () => {
+ const testDatabaseName = 'testdatabase';
+ expect(defaultNameOverride(testDatabaseName)).toHaveProperty(
+ 'connection.database',
+ testDatabaseName,
+ );
+ });
+});
diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.ts b/packages/backend-common/src/database/connectors/defaultNameOverride.ts
new file mode 100644
index 0000000000..6296010c76
--- /dev/null
+++ b/packages/backend-common/src/database/connectors/defaultNameOverride.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Knex } from 'knex';
+
+/**
+ * Provides a partial knex config with database name override.
+ *
+ * Default override for knex database drivers which accept ConnectionConfig
+ * with `connection.database` as the database name field.
+ *
+ * @param name database name to get config override for
+ */
+export default function defaultNameOverride(
+ name: string,
+): Partial {
+ return {
+ connection: {
+ database: name,
+ },
+ };
+}
diff --git a/packages/backend-common/src/database/connectors/index.ts b/packages/backend-common/src/database/connectors/index.ts
new file mode 100644
index 0000000000..f314bb5004
--- /dev/null
+++ b/packages/backend-common/src/database/connectors/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './mysql';
+export * from './postgres';
+export * from './sqlite3';
diff --git a/packages/backend-common/src/database/mysql.test.ts b/packages/backend-common/src/database/connectors/mysql.test.ts
similarity index 100%
rename from packages/backend-common/src/database/mysql.test.ts
rename to packages/backend-common/src/database/connectors/mysql.test.ts
diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts
similarity index 89%
rename from packages/backend-common/src/database/mysql.ts
rename to packages/backend-common/src/database/connectors/mysql.ts
index be4632baf6..60f1e09ce0 100644
--- a/packages/backend-common/src/database/mysql.ts
+++ b/packages/backend-common/src/database/connectors/mysql.ts
@@ -14,11 +14,14 @@
* limitations under the License.
*/
+import knexFactory, { Knex } from 'knex';
+import yn from 'yn';
+
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
-import knexFactory, { Knex } from 'knex';
-import { mergeDatabaseConfig } from './config';
-import yn from 'yn';
+import { mergeDatabaseConfig } from '../config';
+import { DatabaseConnector } from '../types';
+import defaultNameOverride from './defaultNameOverride';
/**
* Creates a knex mysql database connection
@@ -159,3 +162,15 @@ export async function ensureMysqlDatabaseExists(
await admin.destroy();
}
}
+
+/**
+ * MySQL database connector.
+ *
+ * Exposes database connector functionality via an immutable object.
+ */
+export const mysqlConnector: DatabaseConnector = Object.freeze({
+ createClient: createMysqlDatabaseClient,
+ ensureDatabaseExists: ensureMysqlDatabaseExists,
+ createNameOverride: defaultNameOverride,
+ parseConnectionString: parseMysqlConnectionString,
+});
diff --git a/packages/backend-common/src/database/postgres.test.ts b/packages/backend-common/src/database/connectors/postgres.test.ts
similarity index 100%
rename from packages/backend-common/src/database/postgres.test.ts
rename to packages/backend-common/src/database/connectors/postgres.test.ts
diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/connectors/postgres.ts
similarity index 87%
rename from packages/backend-common/src/database/postgres.ts
rename to packages/backend-common/src/database/connectors/postgres.ts
index e05eab86e5..011e40579b 100644
--- a/packages/backend-common/src/database/postgres.ts
+++ b/packages/backend-common/src/database/connectors/postgres.ts
@@ -15,8 +15,11 @@
*/
import knexFactory, { Knex } from 'knex';
+
import { Config } from '@backstage/config';
-import { mergeDatabaseConfig } from './config';
+import { mergeDatabaseConfig } from '../config';
+import { DatabaseConnector } from '../types';
+import defaultNameOverride from './defaultNameOverride';
/**
* Creates a knex postgres database connection
@@ -131,3 +134,15 @@ export async function ensurePgDatabaseExists(
await admin.destroy();
}
}
+
+/**
+ * PostgreSQL database connector.
+ *
+ * Exposes database connector functionality via an immutable object.
+ */
+export const pgConnector: DatabaseConnector = Object.freeze({
+ createClient: createPgDatabaseClient,
+ ensureDatabaseExists: ensurePgDatabaseExists,
+ createNameOverride: defaultNameOverride,
+ parseConnectionString: parsePgConnectionString,
+});
diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/connectors/sqlite3.test.ts
similarity index 100%
rename from packages/backend-common/src/database/sqlite3.test.ts
rename to packages/backend-common/src/database/connectors/sqlite3.test.ts
diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts
similarity index 75%
rename from packages/backend-common/src/database/sqlite3.ts
rename to packages/backend-common/src/database/connectors/sqlite3.ts
index d4169e3899..c9e86c80da 100644
--- a/packages/backend-common/src/database/sqlite3.ts
+++ b/packages/backend-common/src/database/connectors/sqlite3.ts
@@ -13,15 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import path from 'path';
-import { Config } from '@backstage/config';
import { ensureDirSync } from 'fs-extra';
import knexFactory, { Knex } from 'knex';
-import path from 'path';
-import { mergeDatabaseConfig } from './config';
+
+import { Config } from '@backstage/config';
+import { mergeDatabaseConfig } from '../config';
+import { DatabaseConnector } from '../types';
/**
- * Creates a knex sqlite3 database connection
+ * Creates a knex SQLite3 database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
@@ -54,7 +56,7 @@ export function createSqliteDatabaseClient(
}
/**
- * Builds a knex sqlite3 connection config
+ * Builds a knex SQLite3 connection config
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
@@ -99,3 +101,34 @@ export function buildSqliteDatabaseConfig(
return config;
}
+
+/**
+ * Provides a partial knex SQLite3 config to override database name.
+ */
+export function createSqliteNameOverride(name: string): Partial {
+ return {
+ connection: parseSqliteConnectionString(name),
+ };
+}
+
+/**
+ * Produces a partial knex SQLite3 connection config with database name.
+ */
+export function parseSqliteConnectionString(
+ name: string,
+): Knex.Sqlite3ConnectionConfig {
+ return {
+ filename: name,
+ };
+}
+
+/**
+ * SQLite3 database connector.
+ *
+ * Exposes database connector functionality via an immutable object.
+ */
+export const sqlite3Connector: DatabaseConnector = Object.freeze({
+ createClient: createSqliteDatabaseClient,
+ createNameOverride: createSqliteNameOverride,
+ parseConnectionString: parseSqliteConnectionString,
+});
diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts
index c81153aa62..bfb14e7353 100644
--- a/packages/backend-common/src/database/index.ts
+++ b/packages/backend-common/src/database/index.ts
@@ -14,6 +14,17 @@
* limitations under the License.
*/
-export * from './connection';
-export * from './types';
export * from './SingleConnection';
+export * from './DatabaseManager';
+
+/*
+ * Undocumented API surface from connection is being reduced for future deprecation.
+ * Avoid exporting additional symbols.
+ */
+export {
+ createDatabaseClient,
+ createDatabase,
+ ensureDatabaseExists,
+} from './connection';
+
+export type { PluginDatabaseManager } from './types';
diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts
index bf3ceb2786..995f98f27d 100644
--- a/packages/backend-common/src/database/types.ts
+++ b/packages/backend-common/src/database/types.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import { Config } from '@backstage/config';
import { Knex } from 'knex';
/**
@@ -28,3 +29,37 @@ export interface PluginDatabaseManager {
*/
getClient(): Promise;
}
+
+/**
+ * DatabaseConnector manages an underlying Knex database driver.
+ */
+export interface DatabaseConnector {
+ /**
+ * createClient provides an instance of a knex database connector.
+ */
+ createClient(dbConfig: Config, overrides?: Partial): Knex;
+ /**
+ * createNameOverride provides a partial knex config sufficient to override a
+ * database name.
+ */
+ createNameOverride(name: string): Partial;
+ /**
+ * parseConnectionString produces a knex connection config object representing
+ * a database connection string.
+ */
+ parseConnectionString(
+ connectionString: string,
+ client?: string,
+ ): Knex.StaticConnectionConfig;
+ /**
+ * ensureDatabaseExists performs a side-effect to ensure database names passed in are
+ * present.
+ *
+ * Calling this function on databases which already exist should do nothing.
+ * Missing databases should be created if needed.
+ */
+ ensureDatabaseExists?(
+ dbConfig: Config,
+ ...databases: Array
+ ): Promise;
+}
diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md
index c2078fd0bc..e529fe464f 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,59 @@
# @backstage/backend-test-utils
+## 0.1.3
+
+### Patch Changes
+
+- 772dbdb51: Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database
+ connection manager, `DatabaseManager`, which allows developers to configure database
+ connections on a per plugin basis.
+
+ The `backend.database` config path allows you to set `prefix` to use an
+ alternate prefix for automatically generated database names, the default is
+ `backstage_plugin_`. Use `backend.database.plugin.` to set plugin
+ specific database connection configuration, e.g.
+
+ ```yaml
+ backend:
+ database:
+ client: 'pg',
+ prefix: 'custom_prefix_'
+ connection:
+ host: 'localhost'
+ user: 'foo'
+ password: 'bar'
+ plugin:
+ catalog:
+ connection:
+ database: 'database_name_overriden'
+ scaffolder:
+ client: 'sqlite3'
+ connection: ':memory:'
+ ```
+
+ Migrate existing backstage installations by swapping out the database manager in the
+ `packages/backend/src/index.ts` file as shown below:
+
+ ```diff
+ import {
+ - SingleConnectionDatabaseManager,
+ + DatabaseManager,
+ } from '@backstage/backend-common';
+
+ // ...
+
+ function makeCreateEnv(config: Config) {
+ // ...
+ - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
+ + const databaseManager = DatabaseManager.fromConfig(config);
+ // ...
+ }
+ ```
+
+- Updated dependencies
+ - @backstage/backend-common@0.8.3
+ - @backstage/cli@0.7.1
+
## 0.1.2
### Patch Changes
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index c4ff5603f3..3079041c7f 100644
--- a/packages/backend-test-utils/package.json
+++ b/packages/backend-test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
- "version": "0.1.2",
+ "version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,8 +30,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.8.2",
- "@backstage/cli": "^0.7.0",
+ "@backstage/backend-common": "^0.8.3",
+ "@backstage/cli": "^0.7.1",
"@backstage/config": "^0.1.5",
"knex": "^0.95.1",
"mysql2": "^2.2.5",
@@ -41,7 +41,7 @@
"uuid": "^8.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.7.0",
+ "@backstage/cli": "^0.7.1",
"jest": "^26.0.1"
},
"files": [
diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts
index 7c1e6e1bdd..7a51111b02 100644
--- a/packages/backend-test-utils/src/database/TestDatabases.test.ts
+++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts
@@ -71,7 +71,7 @@ describe('TestDatabases', () => {
await input.insert({ x: 'y' }).into('a');
// Look for the mark
- const database = 'backstage_plugin_0';
+ const database = 'backstage_plugin_db0';
const output = knexFactory({
client: 'pg',
connection: { host, port, user, password, database },
@@ -105,7 +105,7 @@ describe('TestDatabases', () => {
await input.insert({ x: 'y' }).into('a');
// Look for the mark
- const database = 'backstage_plugin_0';
+ const database = 'backstage_plugin_db0';
const output = knexFactory({
client: 'pg',
connection: { host, port, user, password, database },
@@ -139,7 +139,7 @@ describe('TestDatabases', () => {
await input.insert({ x: 'y' }).into('a');
// Look for the mark
- const database = 'backstage_plugin_0';
+ const database = 'backstage_plugin_db0';
const output = knexFactory({
client: 'mysql2',
connection: { host, port, user, password, database },
diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts
index ab5846d84d..0ba6bffc0b 100644
--- a/packages/backend-test-utils/src/database/TestDatabases.ts
+++ b/packages/backend-test-utils/src/database/TestDatabases.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
+import { DatabaseManager } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { Knex } from 'knex';
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
@@ -142,7 +142,7 @@ export class TestDatabases {
// Ensure that a unique logical database is created in the instance
const connection = await instance.databaseManager
- .forPlugin(String(this.lastDatabaseIndex++))
+ .forPlugin(String(`db${this.lastDatabaseIndex++}`))
.getClient();
instance.connections.push(connection);
@@ -157,7 +157,7 @@ export class TestDatabases {
if (envVarName) {
const connectionString = process.env[envVarName];
if (connectionString) {
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(
+ const databaseManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
@@ -195,7 +195,7 @@ export class TestDatabases {
properties.dockerImageName!,
);
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(
+ const databaseManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
@@ -220,7 +220,7 @@ export class TestDatabases {
properties.dockerImageName!,
);
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(
+ const databaseManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
@@ -241,7 +241,7 @@ export class TestDatabases {
private async initSqlite(
_properties: TestDatabaseProperties,
): Promise {
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(
+ const databaseManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts
index 791cf09b7b..91b5939765 100644
--- a/packages/backend-test-utils/src/database/types.ts
+++ b/packages/backend-test-utils/src/database/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
+import { DatabaseManager } from '@backstage/backend-common';
import { Knex } from 'knex';
/**
@@ -35,10 +35,9 @@ export type TestDatabaseProperties = {
export type Instance = {
stopContainer?: () => Promise;
- databaseManager: SingleConnectionDatabaseManager;
+ databaseManager: DatabaseManager;
connections: Array;
};
-
export const allDatabases: Record<
TestDatabaseId,
TestDatabaseProperties
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index d43c8f0101..67149c8163 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -29,7 +29,7 @@ import {
getRootLogger,
loadBackendConfig,
notFoundHandler,
- SingleConnectionDatabaseManager,
+ DatabaseManager,
SingleHostDiscovery,
UrlReaders,
useHotMemoize,
@@ -59,7 +59,7 @@ function makeCreateEnv(config: Config) {
root.info(`Created UrlReader ${reader}`);
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
+ const databaseManager = DatabaseManager.fromConfig(config);
const cacheManager = CacheManager.fromConfig(config);
return (plugin: string): PluginEnvironment => {
diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts
index 63a3e53c81..57afe4fefd 100644
--- a/packages/backend/src/plugins/catalog.ts
+++ b/packages/backend/src/plugins/catalog.ts
@@ -14,13 +14,9 @@
* limitations under the License.
*/
-import { useHotCleanup } from '@backstage/backend-common';
import {
CatalogBuilder,
createRouter,
- NextCatalogBuilder,
- runPeriodically,
- createNextRouter,
} from '@backstage/plugin-catalog-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
@@ -28,50 +24,20 @@ import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise {
- /*
- * ** WARNING **
- * DO NOT enable the experimental catalog, it will brick your database migrations.
- * This is solely for internal backstage development.
- */
- if (process.env.EXPERIMENTAL_CATALOG === '1') {
- const builder = new NextCatalogBuilder(env);
- const {
- entitiesCatalog,
- locationAnalyzer,
- processingEngine,
- locationService,
- } = await builder.build();
-
- // TODO(jhaals): run and manage in background.
- await processingEngine.start();
-
- return await createNextRouter({
- entitiesCatalog,
- locationAnalyzer,
- locationService,
- logger: env.logger,
- config: env.config,
- });
- }
-
- const builder = new CatalogBuilder(env);
+ const builder = await CatalogBuilder.create(env);
const {
entitiesCatalog,
- locationsCatalog,
- higherOrderOperation,
locationAnalyzer,
+ processingEngine,
+ locationService,
} = await builder.build();
- useHotCleanup(
- module,
- runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000),
- );
+ await processingEngine.start();
return await createRouter({
entitiesCatalog,
- locationsCatalog,
- higherOrderOperation,
locationAnalyzer,
+ locationService,
logger: env.logger,
config: env.config,
});
diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md
index df350ff288..3c4a2e073c 100644
--- a/packages/catalog-model/CHANGELOG.md
+++ b/packages/catalog-model/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/catalog-model
+## 0.8.3
+
+### Patch Changes
+
+- 1d2ed7844: Removed unused `typescript-json-schema` dependency.
+
## 0.8.2
### Patch Changes
diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md
index 0dab9670de..68644bef87 100644
--- a/packages/catalog-model/api-report.md
+++ b/packages/catalog-model/api-report.md
@@ -10,7 +10,7 @@ import { JsonValue } from '@backstage/config';
import { SerializedError } from '@backstage/errors';
import * as yup from 'yup';
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const analyzeLocationSchema: yup.ObjectSchema<{
location: LocationSpec;
}, object>;
@@ -316,7 +316,7 @@ export { LocationEntityV1alpha1 }
// @public (undocumented)
export const locationEntityV1alpha1Validator: KindValidator;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const locationSchema: yup.ObjectSchema;
// @public (undocumented)
@@ -326,7 +326,7 @@ export type LocationSpec = {
presence?: 'optional' | 'required';
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const locationSpecSchema: yup.ObjectSchema;
// @public (undocumented)
diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json
index e2ea7fad92..3477d57e71 100644
--- a/packages/catalog-model/package.json
+++ b/packages/catalog-model/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
- "version": "0.8.2",
+ "version": "0.8.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -35,13 +35,12 @@
"@types/yup": "^0.29.8",
"ajv": "^7.0.3",
"json-schema": "^0.3.0",
- "typescript-json-schema": "^0.49.0",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
"yup": "^0.29.3"
},
"devDependencies": {
- "@backstage/cli": "^0.7.0",
+ "@backstage/cli": "^0.7.1",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts
index 4d2e602862..3a2fee5089 100644
--- a/packages/catalog-model/src/location/validation.ts
+++ b/packages/catalog-model/src/location/validation.ts
@@ -17,6 +17,7 @@
import * as yup from 'yup';
import { LocationSpec, Location } from './types';
+/** @deprecated */
export const locationSpecSchema = yup
.object({
type: yup.string().required(),
@@ -26,6 +27,7 @@ export const locationSpecSchema = yup
.noUnknown()
.required();
+/** @deprecated */
export const locationSchema = yup
.object({
id: yup.string().required(),
@@ -35,6 +37,7 @@ export const locationSchema = yup
.noUnknown()
.required();
+/** @deprecated */
export const analyzeLocationSchema = yup
.object<{ location: LocationSpec }>({
location: locationSpecSchema,
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 8eae37ef56..32d028a32d 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,33 @@
# @backstage/cli
+## 0.7.1
+
+### Patch Changes
+
+- 3108ff7bf: Make `yarn dev` in newly created backend plugins respect the `PLUGIN_PORT` environment variable.
+
+ You can achieve the same in your created backend plugins by making sure to properly call the port and CORS methods on your service builder. Typically in a file named `src/service/standaloneServer.ts` inside your backend plugin package, replace the following:
+
+ ```ts
+ const service = createServiceBuilder(module)
+ .enableCors({ origin: 'http://localhost:3000' })
+ .addRouter('/my-plugin', router);
+ ```
+
+ With something like the following:
+
+ ```ts
+ let service = createServiceBuilder(module)
+ .setPort(options.port)
+ .addRouter('/my-plugin', router);
+ if (options.enableCors) {
+ service = service.enableCors({ origin: 'http://localhost:3000' });
+ }
+ ```
+
+- Updated dependencies
+ - @backstage/config-loader@0.6.4
+
## 0.7.0
### Minor Changes
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 7d8940f154..a9f34cb7ac 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.7.0",
+ "version": "0.7.1",
"private": false,
"publishConfig": {
"access": "public"
@@ -32,7 +32,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.5",
- "@backstage/config-loader": "^0.6.3",
+ "@backstage/config-loader": "^0.6.4",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
@@ -43,7 +43,7 @@
"@rollup/plugin-yaml": "^2.1.1",
"@spotify/eslint-config-base": "^9.0.0",
"@spotify/eslint-config-react": "^10.0.0",
- "@spotify/eslint-config-typescript": "^9.0.0",
+ "@spotify/eslint-config-typescript": "^10.0.0",
"@sucrase/jest-plugin": "^2.1.0",
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "5.5.x",
@@ -54,7 +54,7 @@
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"@typescript-eslint/eslint-plugin": "^v4.26.0",
- "@typescript-eslint/parser": "^v4.14.0",
+ "@typescript-eslint/parser": "^v4.27.0",
"@yarnpkg/lockfile": "^1.1.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"bfj": "^7.0.2",
@@ -94,8 +94,8 @@
"react-hot-loader": "^4.12.21",
"recursive-readdir": "^2.2.2",
"replace-in-file": "^6.0.0",
- "rollup": "2.33.x",
- "rollup-plugin-dts": "^2.0.1",
+ "rollup": "2.44.x",
+ "rollup-plugin-dts": "^3.0.1",
"rollup-plugin-esbuild": "2.6.x",
"rollup-plugin-peer-deps-external": "^2.2.2",
"rollup-plugin-postcss": "^4.0.0",
@@ -118,9 +118,9 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/backend-common": "^0.8.2",
+ "@backstage/backend-common": "^0.8.3",
"@backstage/config": "^0.1.5",
- "@backstage/core": "^0.7.12",
+ "@backstage/core": "^0.7.13",
"@backstage/dev-utils": "^0.1.17",
"@backstage/test-utils": "^0.1.13",
"@backstage/theme": "^0.2.8",
@@ -139,7 +139,7 @@
"@types/rollup-plugin-postcss": "^2.0.0",
"@types/tar": "^4.0.3",
"@types/webpack": "^4.41.7",
- "@types/webpack-dev-server": "3.11.0",
+ "@types/webpack-dev-server": "^3.11.0",
"@types/yarnpkg__lockfile": "^1.1.4",
"del": "^5.1.0",
"mock-fs": "^4.13.0",
diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts
index 45671c2ead..0ffc1a08ff 100644
--- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts
+++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts
@@ -120,6 +120,7 @@ export class GithubCreateAppServer {
redirect_url: `${baseUrl}/callback`,
hook_attributes: {
url: this.webhookUrl,
+ active: false,
},
};
const manifestJson = JSON.stringify(manifest).replace(/\"/g, '"');
diff --git a/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs
index 6e38965246..765b6aa0d0 100644
--- a/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs
+++ b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs
@@ -34,9 +34,12 @@ export async function startStandaloneServer(
logger,
});
- const service = createServiceBuilder(module)
- .enableCors({ origin: 'http://localhost:3000' })
+ let service = createServiceBuilder(module)
+ .setPort(options.port)
.addRouter('/{{id}}', router);
+ if (options.enableCors) {
+ service = service.enableCors({ origin: 'http://localhost:3000' });
+ }
return await service.start().catch(err => {
logger.error(err);
diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md
index 9f2ae5d214..a3b8bffd8a 100644
--- a/packages/codemods/CHANGELOG.md
+++ b/packages/codemods/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/codemods
+## 0.1.2
+
+### Patch Changes
+
+- 59752e103: Fix execution of `jscodeshift` on windows.
+- Updated dependencies
+ - @backstage/core-components@0.1.3
+
## 0.1.1
### Patch Changes
diff --git a/packages/codemods/package.json b/packages/codemods/package.json
index 2ce0172b75..aa3d358b9e 100644
--- a/packages/codemods/package.json
+++ b/packages/codemods/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/codemods",
"description": "A collection of codemods for Backstage projects",
- "version": "0.1.1",
+ "version": "0.1.2",
"private": true,
"homepage": "https://backstage.io",
"repository": {
diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md
index 67ed7472b5..e0be1eebc8 100644
--- a/packages/config-loader/CHANGELOG.md
+++ b/packages/config-loader/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/config-loader
+## 0.6.4
+
+### Patch Changes
+
+- f00493739: Removed workaround for breaking change in typescript 4.3 and bump `typescript-json-schema` instead. This should again allow the usage of `@items.visibility ` to set the visibility of array items.
+
## 0.6.3
### Patch Changes
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index de0adb4a4a..4a0ede9873 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
- "version": "0.6.3",
+ "version": "0.6.4",
"private": false,
"publishConfig": {
"access": "public",
@@ -37,7 +37,7 @@
"fs-extra": "^9.0.0",
"json-schema": "^0.3.0",
"json-schema-merge-allof": "^0.8.1",
- "typescript-json-schema": "^0.49.0",
+ "typescript-json-schema": "^0.50.1",
"yaml": "^1.9.2",
"yup": "^0.29.3"
},
diff --git a/packages/config-loader/src/lib/schema/collect.test.ts b/packages/config-loader/src/lib/schema/collect.test.ts
index 94fe2154c1..488c8b5fc6 100644
--- a/packages/config-loader/src/lib/schema/collect.test.ts
+++ b/packages/config-loader/src/lib/schema/collect.test.ts
@@ -28,6 +28,15 @@ const mockSchema = {
},
};
+// We need to load in actual TS libraries when using mock-fs.
+// This lookup is to allow the `typescript` dependency to exist either
+// at top level or inside node_modules of typescript-json-schema
+const typescriptModuleDir = path.dirname(
+ require.resolve('typescript/package.json', {
+ paths: [require.resolve('typescript-json-schema')],
+ }),
+);
+
describe('collectConfigSchemas', () => {
afterEach(() => {
mockFs.restore();
@@ -157,9 +166,7 @@ describe('collectConfigSchemas', () => {
},
},
// TypeScript compilation needs to load some real files inside the typescript dir
- '../../node_modules/typescript': (mockFs as any).load(
- '../../node_modules/typescript',
- ),
+ [typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir),
});
await expect(collectConfigSchemas(['a', 'b', 'c'])).resolves.toEqual([
@@ -218,9 +225,7 @@ describe('collectConfigSchemas', () => {
},
},
// TypeScript compilation needs to load some real files inside the typescript dir
- '../../node_modules/typescript': (mockFs as any).load(
- '../../node_modules/typescript',
- ),
+ [typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir),
});
await expect(collectConfigSchemas(['a'])).rejects.toThrow(
diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts
index 5baada2d6f..e12a4c9309 100644
--- a/packages/config-loader/src/lib/schema/collect.ts
+++ b/packages/config-loader/src/lib/schema/collect.ts
@@ -168,23 +168,6 @@ function compileTsSchemas(paths: string[]) {
},
[path.split(sep).join('/')], // Unix paths are expected for all OSes here
) as JsonObject | null;
-
- // This is a workaround for an API change in TypeScript 4.3 where doc comments no
- // longer are represented by a single string, but instead an array of objects.
- // This isn't handled by typescript-json-schema so we do the conversion here instead.
- value = JSON.parse(JSON.stringify(value), (key, prop) => {
- if (key === 'visibility' && Array.isArray(prop)) {
- const text = prop[0]?.text;
- if (!text) {
- const propStr = JSON.stringify(prop);
- throw new Error(
- `Failed conversion of visibility schema, got ${propStr}`,
- );
- }
- return text;
- }
- return prop;
- });
} catch (error) {
if (error.message !== 'type Config not found') {
throw error;
diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md
index b85c9392d7..5aa709121a 100644
--- a/packages/core-app-api/api-report.md
+++ b/packages/core-app-api/api-report.md
@@ -234,6 +234,18 @@ export type ErrorBoundaryFallbackProps = {
resetError: () => void;
};
+// @public (undocumented)
+export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element;
+
+// @public (undocumented)
+export type FeatureFlaggedProps = {
+ children: ReactNode;
+} & ({
+ with: string;
+} | {
+ without: string;
+});
+
// @public (undocumented)
export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null;
diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx
index a5ba5c4bca..ecf06955b0 100644
--- a/packages/core-app-api/src/app/App.tsx
+++ b/packages/core-app-api/src/app/App.tsx
@@ -57,6 +57,7 @@ import {
} from '../extensions/traversal';
import { pluginCollector } from '../plugins/collectors';
import {
+ featureFlagCollector,
routeObjectCollector,
routeParentCollector,
routePathCollector,
@@ -215,7 +216,12 @@ export class PrivateAppImpl implements BackstageApp {
[],
);
- const { routePaths, routeParents, routeObjects } = useMemo(() => {
+ const {
+ routePaths,
+ routeParents,
+ routeObjects,
+ featureFlags,
+ } = useMemo(() => {
const result = traverseElementTree({
root: children,
discoverers: [childDiscoverer, routeElementDiscoverer],
@@ -224,6 +230,7 @@ export class PrivateAppImpl implements BackstageApp {
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
collectedPlugins: pluginCollector,
+ featureFlags: featureFlagCollector,
},
});
@@ -238,7 +245,6 @@ export class PrivateAppImpl implements BackstageApp {
// Initialize APIs once all plugins are available
this.getApiHolder();
-
return result;
}, [children]);
@@ -273,8 +279,14 @@ export class PrivateAppImpl implements BackstageApp {
}
}
}
+
+ // Go through the featureFlags returned from the traversal and
+ // register those now the configApi has been loaded
+ for (const name of featureFlags) {
+ featureFlagsApi.registerFlag({ name, pluginId: '' });
+ }
}
- }, [hasConfigApi, loadedConfig]);
+ }, [hasConfigApi, loadedConfig, featureFlags]);
if ('node' in loadedConfig) {
// Loading or error
diff --git a/packages/core-app-api/src/index.test.ts b/packages/core-app-api/src/index.test.ts
index bec176fda8..654c547fca 100644
--- a/packages/core-app-api/src/index.test.ts
+++ b/packages/core-app-api/src/index.test.ts
@@ -37,6 +37,7 @@ describe('index', () => {
ConfigReader: expect.any(Function),
ErrorAlerter: expect.any(Function),
ErrorApiForwarder: expect.any(Function),
+ FeatureFlagged: expect.any(Function),
GithubAuth: expect.any(Function),
GitlabAuth: expect.any(Function),
GoogleAuth: expect.any(Function),
diff --git a/packages/core-app-api/src/routing/FeatureFlagged.test.tsx b/packages/core-app-api/src/routing/FeatureFlagged.test.tsx
new file mode 100644
index 0000000000..e20af76ae1
--- /dev/null
+++ b/packages/core-app-api/src/routing/FeatureFlagged.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { FeatureFlagged } from './FeatureFlagged';
+import { render } from '@testing-library/react';
+import { ApiProvider, ApiRegistry, LocalStorageFeatureFlags } from '../apis';
+import { featureFlagsApiRef } from '@backstage/core-plugin-api';
+
+const mockFeatureFlagsApi = new LocalStorageFeatureFlags();
+const Wrapper = ({ children }: { children?: React.ReactNode }) => (
+
+ {children}
+
+);
+
+describe('FeatureFlagged', () => {
+ describe('with', () => {
+ it('should render contents when the feature flag is enabled', async () => {
+ jest
+ .spyOn(mockFeatureFlagsApi, 'isActive')
+ .mockImplementation(() => true);
+
+ const { queryByText } = render(
+
+
+
+
BACKSTAGE!
+
+
+ ,
+ );
+
+ expect(await queryByText('BACKSTAGE!')).toBeInTheDocument();
+ });
+ it('should not render contents when the feature flag is disabled', async () => {
+ jest
+ .spyOn(mockFeatureFlagsApi, 'isActive')
+ .mockImplementation(() => false);
+
+ const { queryByText } = render(
+
+
+
+
BACKSTAGE!
+
+
+ ,
+ );
+
+ expect(await queryByText('BACKSTAGE!')).not.toBeInTheDocument();
+ });
+ });
+ describe('without', () => {
+ it('should not render contents when the feature flag is enabled', async () => {
+ jest
+ .spyOn(mockFeatureFlagsApi, 'isActive')
+ .mockImplementation(() => true);
+
+ const { queryByText } = render(
+
+
+
+
BACKSTAGE!
+
+
+ ,
+ );
+
+ expect(await queryByText('BACKSTAGE!')).not.toBeInTheDocument();
+ });
+ it('should render contents when the feature flag is disabled', async () => {
+ jest
+ .spyOn(mockFeatureFlagsApi, 'isActive')
+ .mockImplementation(() => false);
+
+ const { queryByText } = render(
+
+
+
+
BACKSTAGE!
+
+
+ ,
+ );
+
+ expect(await queryByText('BACKSTAGE!')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/core-app-api/src/routing/FeatureFlagged.tsx b/packages/core-app-api/src/routing/FeatureFlagged.tsx
new file mode 100644
index 0000000000..40ef445f92
--- /dev/null
+++ b/packages/core-app-api/src/routing/FeatureFlagged.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ featureFlagsApiRef,
+ useApi,
+ attachComponentData,
+} from '@backstage/core-plugin-api';
+import React, { ReactNode } from 'react';
+
+export type FeatureFlaggedProps = { children: ReactNode } & (
+ | { with: string }
+ | { without: string }
+);
+
+export const FeatureFlagged = (props: FeatureFlaggedProps) => {
+ const { children } = props;
+ const featureFlagApi = useApi(featureFlagsApiRef);
+ const isEnabled =
+ 'with' in props
+ ? featureFlagApi.isActive(props.with)
+ : !featureFlagApi.isActive(props.without);
+ return <>{isEnabled ? children : null}>;
+};
+
+attachComponentData(FeatureFlagged, 'core.featureFlagged', true);
diff --git a/packages/core-app-api/src/routing/FlatRoutes.test.tsx b/packages/core-app-api/src/routing/FlatRoutes.test.tsx
index a9b83d1016..3ed5d5eb3a 100644
--- a/packages/core-app-api/src/routing/FlatRoutes.test.tsx
+++ b/packages/core-app-api/src/routing/FlatRoutes.test.tsx
@@ -17,25 +17,36 @@
import { render, RenderResult } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { MemoryRouter, Route, Routes, useOutlet } from 'react-router-dom';
+import { ApiProvider, ApiRegistry, LocalStorageFeatureFlags } from '../apis';
+import { featureFlagsApiRef } from '@backstage/core-plugin-api';
import { AppContext } from '../app';
import { AppContextProvider } from '../app/AppContext';
import { FlatRoutes } from './FlatRoutes';
+const mockFeatureFlagsApi = new LocalStorageFeatureFlags();
+const Wrapper = ({ children }: { children?: React.ReactNode }) => (
+
+ {children}
+
+);
+
function makeRouteRenderer(node: ReactNode) {
let rendered: RenderResult | undefined = undefined;
return (path: string) => {
const content = (
- ({
- NotFoundErrorPage: () => <>Not Found>,
- }),
- } as unknown) as AppContext
- }
- >
-
-
+
+ ({
+ NotFoundErrorPage: () => <>Not Found>,
+ }),
+ } as unknown) as AppContext
+ }
+ >
+
+
+
);
if (rendered) {
rendered.unmount();
diff --git a/packages/core-app-api/src/routing/FlatRoutes.tsx b/packages/core-app-api/src/routing/FlatRoutes.tsx
index 54ef092965..ad1f764e6c 100644
--- a/packages/core-app-api/src/routing/FlatRoutes.tsx
+++ b/packages/core-app-api/src/routing/FlatRoutes.tsx
@@ -14,53 +14,16 @@
* limitations under the License.
*/
-import React, { ReactNode, Children, isValidElement, Fragment } from 'react';
+import React, { ReactNode } from 'react';
import { useRoutes } from 'react-router-dom';
-import { useApp } from '@backstage/core-plugin-api';
+import { useApp, useElementFilter } from '@backstage/core-plugin-api';
type RouteObject = {
path: string;
- element: JSX.Element;
+ element: ReactNode;
children?: RouteObject[];
};
-// Similar to the same function from react-router, this collects routes from the
-// children, but only the first level of routes
-function createRoutesFromChildren(childrenNode: ReactNode): RouteObject[] {
- return Children.toArray(childrenNode).flatMap(child => {
- if (!isValidElement(child)) {
- return [];
- }
-
- const { children } = child.props;
-
- if (child.type === Fragment) {
- return createRoutesFromChildren(children);
- }
-
- let path = child.props.path as string | undefined;
-
- // TODO(Rugvip): Work around plugins registering empty paths, remove once deprecated routes are gone
- if (path === '') {
- return [];
- }
- path = path?.replace(/\/\*$/, '') ?? '/';
-
- return [
- {
- path,
- element: child,
- children: children && [
- {
- path: '/*',
- element: children,
- },
- ],
- },
- ];
- });
-}
-
type FlatRoutesProps = {
children: ReactNode;
};
@@ -68,14 +31,41 @@ type FlatRoutesProps = {
export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => {
const app = useApp();
const { NotFoundErrorPage } = app.getComponents();
- const routes = createRoutesFromChildren(props.children)
- // Routes are sorted to work around a bug where prefixes are unexpectedly matched
- .sort((a, b) => b.path.localeCompare(a.path))
- // We make sure all routes have '/*' appended, except '/'
- .map(obj => {
- obj.path = obj.path === '/' ? '/' : `${obj.path}/*`;
- return obj;
- });
+ const routes = useElementFilter(props.children, elements =>
+ elements
+ .getElements<{ path?: string; children: ReactNode }>()
+ .flatMap(child => {
+ let path = child.props.path;
+
+ // TODO(Rugvip): Work around plugins registering empty paths, remove once deprecated routes are gone
+ if (path === '') {
+ return [];
+ }
+ path = path?.replace(/\/\*$/, '') ?? '/';
+
+ return [
+ {
+ path,
+ element: child,
+ children: child.props.children
+ ? [
+ {
+ path: '/*',
+ element: child.props.children,
+ },
+ ]
+ : undefined,
+ },
+ ];
+ })
+ // Routes are sorted to work around a bug where prefixes are unexpectedly matched
+ .sort((a, b) => b.path.localeCompare(a.path))
+ // We make sure all routes have '/*' appended, except '/'
+ .map(obj => {
+ obj.path = obj.path === '/' ? '/' : `${obj.path}/*`;
+ return obj;
+ }),
+ );
// TODO(Rugvip): Possibly add a way to skip this, like a noNotFoundPage prop
routes.push({
diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx
index e940a2cada..b9ca45f54a 100644
--- a/packages/core-app-api/src/routing/collectors.tsx
+++ b/packages/core-app-api/src/routing/collectors.tsx
@@ -19,6 +19,7 @@ import { RouteRef } from '@backstage/core-plugin-api';
import { BackstageRouteObject } from './types';
import { getComponentData } from '../extensions';
import { createCollector } from '../extensions/traversal';
+import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged';
function getMountPoint(node: ReactElement): RouteRef | undefined {
const element: ReactNode = node.props?.element;
@@ -171,3 +172,13 @@ export const routeObjectCollector = createCollector(
return parentObj;
},
);
+
+export const featureFlagCollector = createCollector(
+ () => new Set(),
+ (acc, node) => {
+ if (node.type === FeatureFlagged) {
+ const props = node.props as FeatureFlaggedProps;
+ acc.add('with' in props ? props.with : props.without);
+ }
+ },
+);
diff --git a/packages/core-app-api/src/routing/index.ts b/packages/core-app-api/src/routing/index.ts
index 7982333f4b..b37b51e919 100644
--- a/packages/core-app-api/src/routing/index.ts
+++ b/packages/core-app-api/src/routing/index.ts
@@ -15,3 +15,5 @@
*/
export { FlatRoutes } from './FlatRoutes';
+export { FeatureFlagged } from './FeatureFlagged';
+export type { FeatureFlaggedProps } from './FeatureFlagged';
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index 83e5bad9f5..db49ad5adb 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,12 @@
# @backstage/core-components
+## 0.1.3
+
+### Patch Changes
+
+- d2c31b132: Add title prop in SupportButton component
+- d4644f592: Use the Backstage `Link` component in the `Button`
+
## 0.1.2
### Patch Changes
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index 42a98b859a..428c0e9134 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
- "version": "0.1.2",
+ "version": "0.1.3",
"private": false,
"publishConfig": {
"access": "public",
@@ -71,7 +71,7 @@
},
"devDependencies": {
"@backstage/core-app-api": "^0.1.2",
- "@backstage/cli": "^0.7.0",
+ "@backstage/cli": "^0.7.1",
"@backstage/test-utils": "^0.1.13",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
diff --git a/packages/core-components/src/components/Button/Button.tsx b/packages/core-components/src/components/Button/Button.tsx
index ca45b3da7f..c678b9a9db 100644
--- a/packages/core-components/src/components/Button/Button.tsx
+++ b/packages/core-components/src/components/Button/Button.tsx
@@ -14,17 +14,19 @@
* limitations under the License.
*/
-import React, { ComponentProps } from 'react';
-import { Button as MaterialButton } from '@material-ui/core';
-import { Link as RouterLink } from 'react-router-dom';
+import {
+ Button as MaterialButton,
+ ButtonProps as MaterialButtonProps,
+} from '@material-ui/core';
+import React from 'react';
+import { Link, LinkProps } from '../Link';
-type Props = ComponentProps &
- ComponentProps;
+type Props = MaterialButtonProps & Omit;
/**
* Thin wrapper on top of material-ui's Button component
* Makes the Button to utilise react-router
*/
export const Button = React.forwardRef((props, ref) => (
-
+
));
diff --git a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx
index 6d6b9fd477..7c6cab99c1 100644
--- a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx
+++ b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx
@@ -21,8 +21,9 @@ import {
RenderResult,
waitFor,
fireEvent,
+ screen,
} from '@testing-library/react';
-import { wrapInTestApp } from '@backstage/test-utils';
+import { wrapInTestApp, renderInTestApp } from '@backstage/test-utils';
import { SupportButton } from './SupportButton';
const SUPPORT_BUTTON_ID = 'support-button';
@@ -41,6 +42,12 @@ describe('', () => {
);
});
+ it('supports passing a title', async () => {
+ await renderInTestApp();
+ fireEvent.click(screen.getByTestId(SUPPORT_BUTTON_ID));
+ expect(screen.getByText('Custom title')).toBeInTheDocument();
+ });
+
it('shows popover on click', async () => {
let renderResult: RenderResult;
diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx
index 6c6de8abe3..96cc308f74 100644
--- a/packages/core-components/src/components/SupportButton/SupportButton.tsx
+++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx
@@ -24,19 +24,18 @@ import {
ListItem,
ListItemIcon,
ListItemText,
+ Typography,
makeStyles,
Popover,
} from '@material-ui/core';
-import React, {
- Fragment,
- MouseEventHandler,
- PropsWithChildren,
- useState,
-} from 'react';
+import React, { Fragment, MouseEventHandler, useState } from 'react';
import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks';
import { Link } from '../Link';
-type Props = {};
+type SupportButtonProps = {
+ title?: string;
+ children?: React.ReactNode;
+};
const useStyles = makeStyles({
popoverList: {
@@ -78,7 +77,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => {
);
};
-export const SupportButton = ({ children }: PropsWithChildren) => {
+export const SupportButton = ({ title, children }: SupportButtonProps) => {
const { items } = useSupportConfig();
const [popoverOpen, setPopoverOpen] = useState(false);
@@ -121,13 +120,20 @@ export const SupportButton = ({ children }: PropsWithChildren) => {
onClose={popoverCloseHandler}
>
+ {title && (
+
+ {title}
+
+ )}
{React.Children.map(children, (child, i) => (
-
+
{child}
))}
{items &&
- items.map((item, i) => )}
+ items.map((item, i) => (
+
+ ))}