Merge branch 'master' of github.com:spotify/backstage into shmidt-i/app-catalog-tabs-routes-everything-is-connected

This commit is contained in:
Ivan Shmidt
2020-09-01 10:37:18 +02:00
23 changed files with 432 additions and 505 deletions
+1
View File
@@ -92,6 +92,7 @@ dist
# Microsite build output
microsite/build
microsite/i18n
# Gatsby files
.cache/
+3 -1
View File
@@ -6,7 +6,9 @@ title: Add to Marketplace
## Adding a Plugin to the Marketplace
To add a new plugin to the [plugin marketplace](https://backstage.io/plugins)
create a file in `data/plugins` with your plugin's information. Example:
create a file in
[`microsite/data/plugins`](https://github.com/spotify/backstage/tree/master/microsite/data/plugins)
with your plugin's information. Example:
```yaml
---
+9
View File
@@ -0,0 +1,9 @@
---
title: API Docs
author: SDA SE
authorUrl: https://sda.se/
category: Discovery
description: Components to discover and display API entities as an extension to the catalog plugin.
documentation: https://github.com/spotify/backstage/blob/master/plugins/api-docs/README.md
iconUrl: https://github.com/spotify/backstage/raw/master/plugins/api-docs/docs/entity_tab_api.png
npmPackageName: '@backstage/plugin-api-docs'
@@ -0,0 +1,10 @@
---
title: GitHub Pull Requests
author: roadie.io
authorUrl: https://roadie.io/
category: CI
description: View GitHub pull requests for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/github-pull-requests
iconUrl: https://roadie.io/static/7f13bb8d861d8dedc5112fb939d215f9/351f2/GitHub-Mark-Light-120px-plus.png
npmPackageName: '@roadiehq/backstage-plugin-github-pull-requests'
-328
View File
@@ -1,328 +0,0 @@
{
"_comment": "This file is auto-generated by write-translations.js",
"localized-strings": {
"next": "Next",
"previous": "Previous",
"tagline": "An open platform for building developer portals",
"docs": {
"api/backend": {
"title": "Backend"
},
"api/utility-apis": {
"title": "Utility APIs"
},
"architecture-decisions/adrs-adr001": {
"title": "ADR001: Architecture Decision Record (ADR) log",
"sidebar_label": "ADR001"
},
"architecture-decisions/adrs-adr002": {
"title": "ADR002: Default Software Catalog File Format",
"sidebar_label": "ADR002"
},
"architecture-decisions/adrs-adr003": {
"title": "ADR003: Avoid Default Exports and Prefer Named Exports",
"sidebar_label": "ADR003"
},
"architecture-decisions/adrs-adr004": {
"title": "ADR004: Module Export Structure",
"sidebar_label": "ADR004"
},
"architecture-decisions/adrs-adr005": {
"title": "ADR005: Catalog Core Entities",
"sidebar_label": "ADR005"
},
"architecture-decisions/adrs-adr006": {
"title": "ADR006: Avoid React.FC and React.SFC",
"sidebar_label": "ADR006"
},
"architecture-decisions/adrs-adr007": {
"title": "ADR007: Use MSW to mock http requests",
"sidebar_label": "ADR007"
},
"architecture-decisions/adrs-adr008": {
"title": "ADR008: Default Catalog File Name",
"sidebar_label": "ADR008"
},
"architecture-decisions/adrs-overview": {
"title": "Architecture Decision Records (ADR)",
"sidebar_label": "Overview"
},
"auth/add-auth-provider": {
"title": "Adding authentication providers"
},
"auth/auth-backend-classes": {
"title": "Auth backend classes"
},
"auth/auth-backend": {
"title": "Auth backend"
},
"auth/glossary": {
"title": "Glossary"
},
"auth/index": {
"title": "User Authentication and Authorization in Backstage"
},
"auth/oauth": {
"title": "OAuth and OpenID Connect"
},
"conf/defining": {
"title": "Defining Configuration for your Plugin"
},
"conf/index": {
"title": "Static Configuration in Backstage"
},
"conf/reading": {
"title": "Reading Backstage Configuration"
},
"conf/writing": {
"title": "Writing Backstage Configuration Files"
},
"dls/contributing-to-storybook": {
"title": "Contributing to Storybook"
},
"dls/design": {
"title": "Design"
},
"dls/figma": {
"title": "Figma"
},
"FAQ": {
"title": "FAQ"
},
"features/software-catalog/software-catalog-api": {
"title": "API"
},
"features/software-catalog/descriptor-format": {
"title": "Descriptor Format of Catalog Entities",
"sidebar_label": "YAML File Format"
},
"features/software-catalog/extending-the-model": {
"title": "Extending the model"
},
"features/software-catalog/external-integrations": {
"title": "External integrations"
},
"features/software-catalog/software-catalog-overview": {
"title": "Backstage Service Catalog (alpha)",
"sidebar_label": "Backstage Service Catalog"
},
"features/software-catalog/installation": {
"title": "features/software-catalog/installation"
},
"features/software-catalog/system-model": {
"title": "System Model"
},
"features/software-templates/adding-templates": {
"title": "Adding your own Templates"
},
"features/software-templates/extending/extending-preparer": {
"title": "Create your own Preparer"
},
"features/software-templates/extending/extending-publisher": {
"title": "Create your own Publisher"
},
"features/software-templates/extending/extending-templater": {
"title": "Creating your own Templater"
},
"features/software-templates/extending/extending-index": {
"title": "Extending the Scaffolder"
},
"features/software-templates/software-templates-index": {
"title": "Software Templates"
},
"features/software-templates/installation": {
"title": "features/software-templates/installation"
},
"features/techdocs/concepts": {
"title": "Concepts"
},
"features/techdocs/creating-and-publishing": {
"title": "Creating and publishing your docs",
"sidebar_label": "Creating and Publishing Documentation"
},
"features/techdocs/faqs": {
"title": "TechDocs FAQ",
"sidebar_label": "FAQ"
},
"features/techdocs/getting-started": {
"title": "Getting Started"
},
"features/techdocs/techdocs-overview": {
"title": "TechDocs Documentation",
"sidebar_label": "Overview"
},
"getting-started/app-custom-theme": {
"title": "Customize the look-and-feel of your App"
},
"getting-started/configure-app-with-plugins": {
"title": "Configuring App with plugins"
},
"getting-started/create-an-app": {
"title": "Create an App"
},
"getting-started/deployment-k8s": {
"title": "Kubernetes"
},
"getting-started/deployment-other": {
"title": "Other"
},
"getting-started/development-environment": {
"title": "Development Environment"
},
"getting-started/index": {
"title": "Running Backstage Locally"
},
"getting-started/installation": {
"title": "Installation"
},
"overview/adopting": {
"title": "Strategies for adopting"
},
"overview/architecture-overview": {
"title": "Architecture overview"
},
"overview/architecture-terminology": {
"title": "Architecture terminology"
},
"overview/background": {
"title": "The Spotify Story"
},
"overview/roadmap": {
"title": "Project roadmap"
},
"overview/support": {
"title": "Support and community"
},
"overview/vision": {
"title": "Vision"
},
"overview/what-is-backstage": {
"title": "What is Backstage?"
},
"plugins/add-to-marketplace": {
"title": "Add to Marketplace"
},
"plugins/backend-plugin": {
"title": "Backend plugin"
},
"plugins/call-existing-api": {
"title": "Call Existing API"
},
"plugins/create-a-plugin": {
"title": "Create a Backstage Plugin"
},
"plugins/existing-plugins": {
"title": "Existing plugins"
},
"plugins/index": {
"title": "Intro to plugins"
},
"plugins/integrating-plugin-into-service-catalog": {
"title": "Integrate into the catalog service"
},
"plugins/plugin-development": {
"title": "Plugin Development"
},
"plugins/proxying": {
"title": "Proxying"
},
"plugins/publish-private": {
"title": "Publish private"
},
"plugins/publishing": {
"title": "Publishing"
},
"plugins/structure-of-a-plugin": {
"title": "Structure of a Plugin"
},
"plugins/testing": {
"title": "Testing with Jest"
},
"README": {
"title": "README"
},
"reference/createPlugin-feature-flags": {
"title": "createPlugin - feature flags"
},
"reference/createPlugin-router": {
"title": "createPlugin - router"
},
"reference/createPlugin": {
"title": "createPlugin"
},
"reference/utility-apis/AlertApi": {
"title": "reference/utility-apis/AlertApi"
},
"reference/utility-apis/AppThemeApi": {
"title": "reference/utility-apis/AppThemeApi"
},
"reference/utility-apis/BackstageIdentityApi": {
"title": "reference/utility-apis/BackstageIdentityApi"
},
"reference/utility-apis/Config": {
"title": "reference/utility-apis/Config"
},
"reference/utility-apis/ErrorApi": {
"title": "reference/utility-apis/ErrorApi"
},
"reference/utility-apis/FeatureFlagsApi": {
"title": "reference/utility-apis/FeatureFlagsApi"
},
"reference/utility-apis/IdentityApi": {
"title": "reference/utility-apis/IdentityApi"
},
"reference/utility-apis/OAuthApi": {
"title": "reference/utility-apis/OAuthApi"
},
"reference/utility-apis/OAuthRequestApi": {
"title": "reference/utility-apis/OAuthRequestApi"
},
"reference/utility-apis/OpenIdConnectApi": {
"title": "reference/utility-apis/OpenIdConnectApi"
},
"reference/utility-apis/ProfileInfoApi": {
"title": "reference/utility-apis/ProfileInfoApi"
},
"reference/utility-apis/README": {
"title": "Utility API References"
},
"reference/utility-apis/SessionStateApi": {
"title": "reference/utility-apis/SessionStateApi"
},
"reference/utility-apis/StorageApi": {
"title": "reference/utility-apis/StorageApi"
},
"tutorials/journey": {
"title": "Future developer journey"
}
},
"links": {
"GitHub": "GitHub",
"Docs": "Docs",
"Plugins": "Plugins",
"Blog": "Blog",
"Demos": "Demos",
"Newsletter": "Newsletter"
},
"categories": {
"Overview": "Overview",
"Getting Started": "Getting Started",
"Features": "Features",
"Plugins": "Plugins",
"Configuration": "Configuration",
"Auth and identity": "Auth and identity",
"Designing for Backstage": "Designing for Backstage",
"API references": "API references",
"Tutorials": "Tutorials",
"Architecture Decision Records (ADRs)": "Architecture Decision Records (ADRs)",
"Contribute": "Contribute",
"Support": "Support",
"FAQ": "FAQ"
}
},
"pages-strings": {
"Help Translate|recruit community translators for your project": "Help Translate",
"Edit this Doc|recruitment message asking to edit the doc source": "Edit",
"Translate this Doc|recruitment message asking to translate the docs": "Translate"
}
}
+1 -1
View File
@@ -216,7 +216,7 @@ td {
code {
font-family: IBM Plex Mono, Menlo, Monaco, Consolas, Courier New, monospace;
font-weight: 500;
background-color: #0e0e0e;
background-color: #272822;
}
/* .stripe {
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { createDatabase } from './connection';
import { createDatabaseClient } from './connection';
describe('database connection', () => {
const createConfig = (data: any) =>
@@ -26,10 +26,10 @@ describe('database connection', () => {
},
]);
describe(createDatabase, () => {
describe(createDatabaseClient, () => {
it('returns a postgres connection', () => {
expect(
createDatabase(
createDatabaseClient(
createConfig({
client: 'pg',
connection: {
@@ -45,7 +45,7 @@ describe('database connection', () => {
it('returns an sqlite connection', () => {
expect(
createDatabase(
createDatabaseClient(
createConfig({
client: 'sqlite3',
connection: ':memory:',
@@ -56,7 +56,7 @@ describe('database connection', () => {
it('tries to create a mysql connection as a passthrough', () => {
expect(() =>
createDatabase(
createDatabaseClient(
createConfig({
client: 'mysql',
connection: {
@@ -72,7 +72,7 @@ describe('database connection', () => {
it('accepts overrides', () => {
expect(
createDatabase(
createDatabaseClient(
createConfig({
client: 'pg',
connection: {
@@ -93,7 +93,7 @@ describe('database connection', () => {
it('throws an error without a client', () => {
expect(() =>
createDatabase(
createDatabaseClient(
createConfig({
connection: '',
}),
@@ -103,7 +103,7 @@ describe('database connection', () => {
it('throws an error without a connection', () => {
expect(() =>
createDatabase(
createDatabaseClient(
createConfig({
client: 'pg',
}),
@@ -15,30 +15,36 @@
*/
import knex from 'knex';
import { ConfigReader } from '@backstage/config';
import { Config } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
import { createPgDatabase } from './postgres';
import { createSqlite3Database } from './sqlite3';
import { createPgDatabaseClient } from './postgres';
import { createSqliteDatabaseClient } from './sqlite3';
type DatabaseClient = 'pg' | 'sqlite3' | string;
/**
* Creates a knex database connection
*
* @param config The database config
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createDatabase(
config: ConfigReader,
export function createDatabaseClient(
dbConfig: Config,
overrides?: Partial<knex.Config>,
) {
const client: DatabaseClient = config.getString('client');
const client: DatabaseClient = dbConfig.getString('client');
if (client === 'pg') {
return createPgDatabase(config, overrides);
return createPgDatabaseClient(dbConfig, overrides);
} else if (client === 'sqlite3') {
return createSqlite3Database(config);
return createSqliteDatabaseClient(dbConfig);
}
return knex(mergeDatabaseConfig(config.get(), overrides));
return knex(mergeDatabaseConfig(dbConfig.get(), overrides));
}
/**
* Alias for createDatabaseClient
* @deprecated Use createDatabaseClient instead
*/
export const createDatabase = createDatabaseClient;
@@ -14,15 +14,26 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { Config, ConfigReader } from '@backstage/config';
import {
parsePgConnectionString,
createPgDatabaseClient,
buildPgDatabaseConfig,
createPgDatabase,
getPgConnectionConfig,
parsePgConnectionString,
} from './postgres';
describe('postgres', () => {
const createConfig = (connection: any) =>
const createMockConnection = () => ({
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
});
const createMockConnectionString = () =>
'postgresql://foo:bar@acme:5432/foodb';
const createConfig = (connection: any): Config =>
ConfigReader.fromConfigs([
{
context: '',
@@ -35,86 +46,58 @@ describe('postgres', () => {
describe(buildPgDatabaseConfig, () => {
it('builds a postgres config', () => {
expect(
buildPgDatabaseConfig(
createConfig({
host: 'acme',
user: 'foo',
password: 'bar',
port: '5432',
database: 'foodb',
}),
),
).toEqual({
const mockConnection = createMockConnection();
expect(buildPgDatabaseConfig(createConfig(mockConnection))).toEqual({
client: 'pg',
connection: {
host: 'acme',
user: 'foo',
password: 'bar',
port: '5432',
database: 'foodb',
},
connection: mockConnection,
useNullAsDefault: true,
});
});
it('builds a connection string config', () => {
expect(
buildPgDatabaseConfig(
createConfig('postgresql://foo:bar@acme:5432/foodb'),
),
).toEqual({
client: 'pg',
connection: 'postgresql://foo:bar@acme:5432/foodb',
useNullAsDefault: true,
});
const mockConnectionString = createMockConnectionString();
expect(buildPgDatabaseConfig(createConfig(mockConnectionString))).toEqual(
{
client: 'pg',
connection: mockConnectionString,
useNullAsDefault: true,
},
);
});
it('overrides the database name', () => {
const mockConnection = createMockConnection();
expect(
buildPgDatabaseConfig(
createConfig({
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foo',
}),
{ connection: { database: 'foodb' } },
),
buildPgDatabaseConfig(createConfig(mockConnection), {
connection: { database: 'other_db' },
}),
).toEqual({
client: 'pg',
connection: {
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foodb',
...mockConnection,
database: 'other_db',
},
useNullAsDefault: true,
});
});
it('adds additional config settings', () => {
const mockConnection = createMockConnection();
expect(
buildPgDatabaseConfig(
createConfig({
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foo',
}),
{
connection: { database: 'foodb' },
pool: { min: 0, max: 7 },
debug: true,
},
),
buildPgDatabaseConfig(createConfig(mockConnection), {
connection: { database: 'other_db' },
pool: { min: 0, max: 7 },
debug: true,
}),
).toEqual({
client: 'pg',
connection: {
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foodb',
...mockConnection,
database: 'other_db',
},
useNullAsDefault: true,
pool: { min: 0, max: 7 },
@@ -123,37 +106,72 @@ describe('postgres', () => {
});
it('overrides the database from connection string', () => {
const mockConnectionString = createMockConnectionString();
const mockConnection = createMockConnection();
expect(
buildPgDatabaseConfig(
createConfig('postgresql://postgres:pass@localhost:5432/dbname'),
{ connection: { database: 'foodb' } },
),
buildPgDatabaseConfig(createConfig(mockConnectionString), {
connection: { database: 'other_db' },
}),
).toEqual({
client: 'pg',
connection: {
host: 'localhost',
user: 'postgres',
password: 'pass',
...mockConnection,
port: '5432',
database: 'foodb',
database: 'other_db',
},
useNullAsDefault: true,
});
});
});
describe(createPgDatabase, () => {
describe(getPgConnectionConfig, () => {
it('returns the connection object back', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
expect(getPgConnectionConfig(config)).toEqual(mockConnection);
});
it('does not parse the connection string', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
expect(getPgConnectionConfig(config, true)).toEqual(mockConnection);
});
it('automatically parses the connection string', () => {
const mockConnection = createMockConnection();
const mockConnectionString = createMockConnectionString();
const config = createConfig(mockConnectionString);
expect(getPgConnectionConfig(config)).toEqual({
...mockConnection,
port: '5432',
});
});
it('parses the connection string', () => {
const mockConnection = createMockConnection();
const mockConnectionString = createMockConnectionString();
const config = createConfig(mockConnectionString);
expect(getPgConnectionConfig(config, true)).toEqual({
...mockConnection,
port: '5432',
});
});
});
describe(createPgDatabaseClient, () => {
it('creates a postgres knex instance', () => {
expect(
createPgDatabase(
createPgDatabaseClient(
createConfig({
client: 'pg',
connection: {
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
},
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
}),
),
).toBeTruthy();
@@ -161,7 +179,7 @@ describe('postgres', () => {
it('attempts to read an ssl cert', () => {
expect(() =>
createPgDatabase(
createPgDatabaseClient(
createConfig(
'postgresql://postgres:pass@localhost:5432/dbname?sslrootcert=/path/to/file',
),
@@ -14,18 +14,18 @@
* limitations under the License.
*/
import knex from 'knex';
import { ConfigReader } from '@backstage/config';
import knex, { PgConnectionConfig } from 'knex';
import { Config } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
/**
* Creates a knex sqlite3 database connection
* Creates a knex postgres database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createPgDatabase(
dbConfig: ConfigReader,
export function createPgDatabaseClient(
dbConfig: Config,
overrides?: knex.Config,
) {
const knexConfig = buildPgDatabaseConfig(dbConfig, overrides);
@@ -40,26 +40,43 @@ export function createPgDatabase(
* @param overrides Additional options to merge with the config
*/
export function buildPgDatabaseConfig(
dbConfig: ConfigReader,
dbConfig: Config,
overrides?: knex.Config,
) {
const connection = dbConfig.get('connection') as any;
return mergeDatabaseConfig(
dbConfig.get(),
{
// Only parse the connection string when overrides are provided
connection:
overrides &&
(typeof connection === 'string' || connection instanceof String)
? parsePgConnectionString(connection as string)
: connection,
connection: getPgConnectionConfig(dbConfig, !!overrides),
useNullAsDefault: true,
},
overrides,
);
}
/**
* Gets the postgres connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explictly control connection string parsing
*/
export function getPgConnectionConfig(
dbConfig: Config,
parseConnectionString?: boolean,
): PgConnectionConfig | string {
const connection = dbConfig.get('connection') as any;
const isConnectionString =
typeof connection === 'string' || connection instanceof String;
const autoParse = typeof parseConnectionString !== 'boolean';
const shouldParseConnectionString = autoParse
? isConnectionString
: parseConnectionString && isConnectionString;
return shouldParseConnectionString
? parsePgConnectionString(connection as string)
: connection;
}
/**
* Parses a connection string using pg-connection-string
*
@@ -15,7 +15,10 @@
*/
import { ConfigReader } from '@backstage/config';
import { buildSqlite3DatabaseConfig, createSqlite3Database } from './sqlite3';
import {
buildSqliteDatabaseConfig,
createSqliteDatabaseClient,
} from './sqlite3';
describe('sqlite3', () => {
const createConfig = (connection: any) =>
@@ -29,9 +32,9 @@ describe('sqlite3', () => {
},
]);
describe(buildSqlite3DatabaseConfig, () => {
describe(buildSqliteDatabaseConfig, () => {
it('buidls a string connection', () => {
expect(buildSqlite3DatabaseConfig(createConfig(':memory:'))).toEqual({
expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
@@ -40,7 +43,7 @@ describe('sqlite3', () => {
it('builds a filename connection', () => {
expect(
buildSqlite3DatabaseConfig(
buildSqliteDatabaseConfig(
createConfig({
filename: '/path/to/foo',
}),
@@ -56,7 +59,7 @@ describe('sqlite3', () => {
it('replaces the connection with an override', () => {
expect(
buildSqlite3DatabaseConfig(createConfig(':memory:'), {
buildSqliteDatabaseConfig(createConfig(':memory:'), {
connection: { filename: '/path/to/foo' },
}),
).toEqual({
@@ -69,10 +72,10 @@ describe('sqlite3', () => {
});
});
describe(createSqlite3Database, () => {
describe(createSqliteDatabaseClient, () => {
it('creates an in memory knex instance', () => {
expect(
createSqlite3Database(
createSqliteDatabaseClient(
createConfig({
client: 'sqlite3',
connection: ':memory:',
@@ -15,7 +15,7 @@
*/
import knex from 'knex';
import { ConfigReader } from '@backstage/config';
import { Config } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
/**
@@ -24,11 +24,11 @@ import { mergeDatabaseConfig } from './config';
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createSqlite3Database(
dbConfig: ConfigReader,
export function createSqliteDatabaseClient(
dbConfig: Config,
overrides?: knex.Config,
) {
const knexConfig = buildSqlite3DatabaseConfig(dbConfig, overrides);
const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides);
const database = knex(knexConfig);
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
@@ -44,8 +44,8 @@ export function createSqlite3Database(
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function buildSqlite3DatabaseConfig(
dbConfig: ConfigReader,
export function buildSqliteDatabaseConfig(
dbConfig: Config,
overrides?: knex.Config,
) {
return mergeDatabaseConfig(
+8 -5
View File
@@ -23,7 +23,7 @@
*/
import {
createDatabase,
createDatabaseClient,
createServiceBuilder,
loadBackendConfig,
getRootLogger,
@@ -48,11 +48,14 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
const database = createDatabase(config.getConfig('backend.database'), {
connection: {
database: `backstage_plugin_${plugin}`,
const database = createDatabaseClient(
config.getConfig('backend.database'),
{
connection: {
database: `backstage_plugin_${plugin}`,
},
},
});
);
return { logger, database, config };
};
}
+1 -1
View File
@@ -36,7 +36,7 @@ export async function buildBundle(options: BuildOptions) {
const { statsJsonEnabled } = options;
const paths = resolveBundlingPaths(options);
const config = createConfig(paths, {
const config = await createConfig(paths, {
...options,
checksEnabled: false,
isDev: false,
+43 -2
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
@@ -25,6 +26,9 @@ import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { BundlingOptions, BackendBundlingOptions } from './types';
import { version } from '../../lib/version';
import { paths as cliPaths } from '../../lib/paths';
import { runPlain } from '../run';
export function resolveBaseUrl(config: Config): URL {
const baseUrl = config.getString('app.baseUrl');
@@ -35,10 +39,40 @@ export function resolveBaseUrl(config: Config): URL {
}
}
export function createConfig(
async function readBuildInfo() {
const timestamp = Date.now();
let commit = 'unknown';
try {
commit = await runPlain('git', 'rev-parse', 'HEAD');
} catch (error) {
console.warn(`WARNING: Failed to read git commit, ${error}`);
}
let gitVersion = 'unknown';
try {
gitVersion = await runPlain('git', 'describe', '--always');
} catch (error) {
console.warn(`WARNING: Failed to describe git version, ${error}`);
}
const { version: packageVersion } = await fs.readJson(
cliPaths.resolveTarget('package.json'),
);
return {
cliVersion: version,
gitVersion,
packageVersion,
timestamp,
commit,
};
}
export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): webpack.Configuration {
): Promise<webpack.Configuration> {
const { checksEnabled, isDev } = options;
const { plugins, loaders } = transforms(options);
@@ -81,6 +115,13 @@ export function createConfig(
}),
);
const buildInfo = await readBuildInfo();
plugins.push(
new webpack.DefinePlugin({
'process.env.BUILD_INFO': JSON.stringify(buildInfo),
}),
);
return {
mode: isDev ? 'development' : 'production',
profile: false,
+1 -1
View File
@@ -30,7 +30,7 @@ export async function serveBundle(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
const pkgPath = paths.targetPackageJson;
const pkg = await fs.readJson(pkgPath);
const config = createConfig(paths, {
const config = await createConfig(paths, {
...options,
isDev: true,
baseUrl: url,
@@ -7,7 +7,7 @@
*/
import {
createDatabase,
createDatabaseClient,
createServiceBuilder,
loadBackendConfig,
getRootLogger,
@@ -27,11 +27,14 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
const database = createDatabase(config.getConfig('backend.database'), {
connection: {
database: `backstage_plugin_${plugin}`,
const database = createDatabaseClient(
config.getConfig('backend.database'),
{
connection: {
database: `backstage_plugin_${plugin}`,
},
},
});
);
return { logger, database, config };
};
}
@@ -75,12 +75,7 @@ const columns: TableColumn<Entity>[] = [
<>
{entity.metadata.tags &&
entity.metadata.tags.map(t => (
<Chip
key={t}
label={t}
color="secondary"
style={{ marginBottom: '0px' }}
/>
<Chip key={t} label={t} style={{ marginBottom: '0px' }} />
))}
</>
),
@@ -64,7 +64,7 @@ export const ScaffolderPage: React.FC<{}> = () => {
}, [error, errorApi]);
return (
<Page theme={pageTheme.other}>
<Page theme={pageTheme.home}>
<Header
pageTitleOverride="Create a new component"
title={
@@ -95,7 +95,7 @@ export const ScaffolderPage: React.FC<{}> = () => {
<Typography variant="body2">
Shoot! Looks like you don't have any templates. Check out the
documentation{' '}
<Link href="docs/backstage/features/software-templates/adding-templates">
<Link href="https://backstage.io/docs/features/software-templates/adding-templates">
here!
</Link>
</Typography>
@@ -136,7 +136,7 @@ export const TemplatePage = () => {
}
return (
<Page theme={pageTheme.other}>
<Page theme={pageTheme.home}>
<Header
pageTitleOverride="Create a new component"
title={
@@ -0,0 +1,47 @@
/*
* Copyright 2020 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 { Generators, TechdocsGenerator } from './';
import { getVoidLogger } from '@backstage/backend-common';
const logger = getVoidLogger();
const mockEntity = {
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'testName',
},
};
describe('generators', () => {
it('should return error if no generator is registered', async () => {
const generators = new Generators();
expect(() => generators.get(mockEntity)).toThrowError(
'No generator registered for entity: "techdocs"',
);
});
it('should return correct registered generator', async () => {
const generators = new Generators();
const techdocs = new TechdocsGenerator(logger);
generators.register('techdocs', techdocs);
expect(generators.get(mockEntity)).toBe(techdocs);
});
});
@@ -15,32 +15,29 @@
*/
import {
GeneratorBase,
SupportedGeneratorKey,
GeneratorBuilder,
} from './types';
import { Entity } from '@backstage/catalog-model';
import { getGeneratorKey } from './helpers';
export class Generators implements GeneratorBuilder {
private generatorMap = new Map<SupportedGeneratorKey, GeneratorBase>();
register(templaterKey: SupportedGeneratorKey, templater: GeneratorBase) {
this.generatorMap.set(templaterKey, templater);
}
get(entity: Entity): GeneratorBase {
const generatorKey = getGeneratorKey(entity);
const generator = this.generatorMap.get(generatorKey);
if (!generator) {
throw new Error(
`No generator registered for entity: "${generatorKey}"`,
);
}
return generator;
}
GeneratorBase,
SupportedGeneratorKey,
GeneratorBuilder,
} from './types';
import { Entity } from '@backstage/catalog-model';
import { getGeneratorKey } from './helpers';
export class Generators implements GeneratorBuilder {
private generatorMap = new Map<SupportedGeneratorKey, GeneratorBase>();
register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase) {
this.generatorMap.set(generatorKey, generator);
}
get(entity: Entity): GeneratorBase {
const generatorKey = getGeneratorKey(entity);
const generator = this.generatorMap.get(generatorKey);
if (!generator) {
throw new Error(`No generator registered for entity: "${generatorKey}"`);
}
return generator;
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2020 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 Stream, { PassThrough } from 'stream';
import os from 'os';
import Docker from 'dockerode';
import { runDockerContainer, getGeneratorKey } from './helpers';
const mockEntity = {
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'testName',
},
};
const mockDocker = new Docker() as jest.Mocked<Docker>;
describe('helpers', () => {
describe('getGeneratorKey', () => {
it('should return techdocs as the only generator key', () => {
const key = getGeneratorKey(mockEntity);
expect(key).toBe('techdocs');
});
});
describe('runDockerContainer', () => {
beforeEach(() => {
jest.spyOn(mockDocker, 'pull').mockImplementation((async (
_image: string,
_something: any,
handler: (err: Error | undefined, stream: PassThrough) => void,
) => {
const mockStream = new PassThrough();
handler(undefined, mockStream);
mockStream.end();
}) as any);
jest
.spyOn(mockDocker, 'run')
.mockResolvedValue([{ Error: null, StatusCode: 0 }]);
});
const imageName = 'spotify/techdocs';
const args = ['build', '-d', '/result'];
const docsDir = os.tmpdir();
const resultDir = os.tmpdir();
it('should pull the techdocs docker container', async () => {
await runDockerContainer({
imageName,
args,
docsDir,
resultDir,
dockerClient: mockDocker,
});
expect(mockDocker.pull).toHaveBeenCalledWith(
imageName,
{},
expect.any(Function),
);
});
it('should run the techdocs docker container', async () => {
await runDockerContainer({
imageName,
args,
docsDir,
resultDir,
dockerClient: mockDocker,
});
expect(mockDocker.run).toHaveBeenCalledWith(
imageName,
args,
expect.any(Stream),
{
Volumes: {
'/content': {},
'/result': {},
},
WorkingDir: '/content',
HostConfig: {
Binds: [`${docsDir}:/content`, `${resultDir}:/result`],
},
},
);
});
});
});