Solved merge conflicts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-api-docs",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,10 +20,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@kyma-project/asyncapi-react": "^0.11.0",
|
||||
"@material-icons/font": "^1.0.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
@@ -39,9 +39,9 @@
|
||||
"swagger-ui-react": "^3.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-app-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,8 +20,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
@@ -30,7 +30,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^0.19.5",
|
||||
"supertest": "^4.0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,8 +20,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
@@ -49,7 +49,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,13 +20,14 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.0",
|
||||
"git-url-parse": "^11.2.0",
|
||||
"knex": "^0.21.1",
|
||||
"lodash": "^4.17.15",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -39,7 +40,8 @@
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
|
||||
@@ -343,7 +343,14 @@ export class CommonDatabase implements Database {
|
||||
entityName?: string,
|
||||
message?: string,
|
||||
): Promise<void> {
|
||||
return this.database<DatabaseLocationUpdateLogEvent>(
|
||||
// Remove log entries older than a day
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 1);
|
||||
await this.database<DatabaseLocationUpdateLogEvent>('location_update_log')
|
||||
.where('created_at', '<', cutoff.toISOString())
|
||||
.del();
|
||||
|
||||
await this.database<DatabaseLocationUpdateLogEvent>(
|
||||
'location_update_log',
|
||||
).insert({
|
||||
status,
|
||||
|
||||
@@ -27,7 +27,6 @@ import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEn
|
||||
import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
|
||||
import { FileReaderProcessor } from './processors/FileReaderProcessor';
|
||||
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
|
||||
import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor';
|
||||
import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor';
|
||||
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
|
||||
import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor';
|
||||
@@ -67,18 +66,19 @@ export class LocationReaders implements LocationReader {
|
||||
private readonly rulesEnforcer: CatalogRulesEnforcer;
|
||||
|
||||
static defaultProcessors(options: {
|
||||
logger: Logger;
|
||||
config?: Config;
|
||||
entityPolicy?: EntityPolicy;
|
||||
}): LocationProcessor[] {
|
||||
const {
|
||||
logger,
|
||||
config = new ConfigReader({}, 'missing-config'),
|
||||
entityPolicy = new EntityPolicies(),
|
||||
} = options;
|
||||
return [
|
||||
StaticLocationProcessor.fromConfig(config),
|
||||
new FileReaderProcessor(),
|
||||
new GithubReaderProcessor(config),
|
||||
GithubApiReaderProcessor.fromConfig(config),
|
||||
GithubReaderProcessor.fromConfig(config, logger),
|
||||
new GitlabApiReaderProcessor(config),
|
||||
new GitlabReaderProcessor(),
|
||||
new BitbucketApiReaderProcessor(config),
|
||||
@@ -94,7 +94,7 @@ export class LocationReaders implements LocationReader {
|
||||
constructor({
|
||||
logger = getVoidLogger(),
|
||||
config,
|
||||
processors = LocationReaders.defaultProcessors({ config }),
|
||||
processors = LocationReaders.defaultProcessors({ logger, config }),
|
||||
}: Options) {
|
||||
this.logger = logger;
|
||||
this.processors = processors;
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* 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 { LocationSpec } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
getRawUrl,
|
||||
getRequestOptions,
|
||||
GithubApiReaderProcessor,
|
||||
ProviderConfig,
|
||||
readConfig,
|
||||
} from './GithubApiReaderProcessor';
|
||||
|
||||
describe('GithubApiReaderProcessor', () => {
|
||||
describe('getRequestOptions', () => {
|
||||
it('sets the correct API version', () => {
|
||||
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
|
||||
expect((getRequestOptions(config).headers as any).Accept).toEqual(
|
||||
'application/vnd.github.v3.raw',
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: ProviderConfig = {
|
||||
target: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: ProviderConfig = {
|
||||
target: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRawUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
|
||||
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
|
||||
});
|
||||
|
||||
it('passes through the happy path', () => {
|
||||
const config: ProviderConfig = {
|
||||
target: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
};
|
||||
expect(
|
||||
getRawUrl(
|
||||
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readConfig', () => {
|
||||
function config(
|
||||
providers: { target: string; apiBaseUrl?: string; token?: string }[],
|
||||
) {
|
||||
return ConfigReader.fromConfigs([
|
||||
{
|
||||
context: '',
|
||||
data: {
|
||||
catalog: { processors: { githubApi: { providers } } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
it('adds a default GitHub entry when missing', () => {
|
||||
const output = readConfig(config([]));
|
||||
expect(output).toEqual([
|
||||
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the correct GitHub API base URL when missing', () => {
|
||||
const output = readConfig(config([{ target: 'https://github.com' }]));
|
||||
expect(output).toEqual([
|
||||
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects custom targets with no API base URL', () => {
|
||||
expect(() =>
|
||||
readConfig(config([{ target: 'https://ghe.company.com' }])),
|
||||
).toThrow(
|
||||
'Provider at https://ghe.company.com must configure an explicit apiBaseUrl',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
expect(() => readConfig(config([{ target: 7 } as any]))).toThrow(
|
||||
/target/,
|
||||
);
|
||||
expect(() => readConfig(config([{ noTarget: '7' } as any]))).toThrow(
|
||||
/target/,
|
||||
);
|
||||
expect(() =>
|
||||
readConfig(
|
||||
config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
|
||||
),
|
||||
).toThrow(/apiBaseUrl/);
|
||||
expect(() =>
|
||||
readConfig(config([{ target: 'https://github.com', token: 7 } as any])),
|
||||
).toThrow(/token/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('implementation', () => {
|
||||
it('rejects unknown types', async () => {
|
||||
const processor = new GithubApiReaderProcessor([
|
||||
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
|
||||
]);
|
||||
const location: LocationSpec = {
|
||||
type: 'not-github/api',
|
||||
target: 'https://github.com',
|
||||
};
|
||||
await expect(
|
||||
processor.readLocation(location, false, () => {}),
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
const processor = new GithubApiReaderProcessor([
|
||||
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
|
||||
]);
|
||||
const location: LocationSpec = {
|
||||
type: 'github/api',
|
||||
target: 'https://not.github.com/apa',
|
||||
};
|
||||
await expect(
|
||||
processor.readLocation(location, false, () => {}),
|
||||
).rejects.toThrow(
|
||||
/There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
/*
|
||||
* 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 { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import fetch, { HeadersInit, RequestInit } from 'node-fetch';
|
||||
import * as result from './results';
|
||||
import { LocationProcessor, LocationProcessorEmit } from './types';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single GitHub API provider.
|
||||
*/
|
||||
export type ProviderConfig = {
|
||||
/**
|
||||
* The prefix of the target that this matches on, e.g. "https://github.com",
|
||||
* with no trailing slash.
|
||||
*/
|
||||
target: string;
|
||||
|
||||
/**
|
||||
* The base URL of the API of this provider, e.g. "https://api.github.com",
|
||||
* with no trailing slash.
|
||||
*/
|
||||
apiBaseUrl: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests to this provider.
|
||||
*
|
||||
* If no token is specified, anonymous API access is used.
|
||||
*/
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export function getRequestOptions(provider: ProviderConfig): RequestInit {
|
||||
const headers: HeadersInit = {
|
||||
Accept: 'application/vnd.github.v3.raw',
|
||||
};
|
||||
|
||||
if (provider.token) {
|
||||
headers.Authorization = `token ${provider.token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
// Converts for example
|
||||
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
|
||||
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
|
||||
export function getRawUrl(target: string, provider: ProviderConfig): URL {
|
||||
try {
|
||||
const oldPath = new URL(target).pathname.split('/');
|
||||
const [, userOrOrg, repoName, blobOrRaw, ref, ...restOfPath] = oldPath;
|
||||
|
||||
if (
|
||||
!userOrOrg ||
|
||||
!repoName ||
|
||||
(blobOrRaw !== 'blob' && blobOrRaw !== 'raw') ||
|
||||
!restOfPath.join('/').match(/\.ya?ml$/)
|
||||
) {
|
||||
throw new Error('Wrong URL or Invalid file path');
|
||||
}
|
||||
|
||||
// Transform to API path
|
||||
const newPath = [
|
||||
'repos',
|
||||
userOrOrg,
|
||||
repoName,
|
||||
'contents',
|
||||
...restOfPath,
|
||||
].join('/');
|
||||
return new URL(`${provider.apiBaseUrl}/${newPath}?ref=${ref}`);
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function readConfig(configRoot: Config): ProviderConfig[] {
|
||||
const providers: ProviderConfig[] = [];
|
||||
|
||||
// In a previous version of the configuration, we only supported github,
|
||||
// and the "privateToken" key held the token to use for it. The new
|
||||
// configuration method is to use the "providers" key instead.
|
||||
const config = configRoot.getOptionalConfig('catalog.processors.githubApi');
|
||||
const providerConfigs = config?.getOptionalConfigArray('providers') ?? [];
|
||||
const legacyToken = config?.getOptionalString('privateToken');
|
||||
|
||||
// First read all the explicit providers
|
||||
for (const providerConfig of providerConfigs) {
|
||||
const target = providerConfig.getString('target').replace(/\/+$/, '');
|
||||
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
|
||||
const token = providerConfig.getOptionalString('token');
|
||||
|
||||
if (apiBaseUrl) {
|
||||
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
|
||||
} else if (target === 'https://github.com') {
|
||||
apiBaseUrl = 'https://api.github.com';
|
||||
} else {
|
||||
throw new Error(
|
||||
`Provider at ${target} must configure an explicit apiBaseUrl`,
|
||||
);
|
||||
}
|
||||
|
||||
providers.push({ target, apiBaseUrl, token });
|
||||
}
|
||||
|
||||
// If no explicit github.com provider was added, put one in the list as
|
||||
// a convenience
|
||||
if (!providers.some(p => p.target === 'https://github.com')) {
|
||||
providers.push({
|
||||
target: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
token: legacyToken,
|
||||
});
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* A processor that adds the ability to read files from GitHub v3 APIs, such as
|
||||
* the one exposed by GitHub itself.
|
||||
*/
|
||||
export class GithubApiReaderProcessor implements LocationProcessor {
|
||||
private providers: ProviderConfig[];
|
||||
|
||||
static fromConfig(config: Config) {
|
||||
return new GithubApiReaderProcessor(readConfig(config));
|
||||
}
|
||||
|
||||
constructor(providers: ProviderConfig[]) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
optional: boolean,
|
||||
emit: LocationProcessorEmit,
|
||||
): Promise<boolean> {
|
||||
if (location.type !== 'github/api') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const provider = this.providers.find(p =>
|
||||
location.target.startsWith(`${p.target}/`),
|
||||
);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.github.processors.githubApi.`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = getRawUrl(location.target, provider);
|
||||
const options = getRequestOptions(provider);
|
||||
const response = await fetch(url.toString(), options);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.buffer();
|
||||
emit(result.data(location, data));
|
||||
} else {
|
||||
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
if (!optional) {
|
||||
emit(result.notFoundError(location, message));
|
||||
}
|
||||
} else {
|
||||
emit(result.generalError(location, message));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
|
||||
emit(result.generalError(location, message));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* 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 { getVoidLogger } from '@backstage/backend-common';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
getApiRequestOptions,
|
||||
getApiUrl,
|
||||
getRawRequestOptions,
|
||||
getRawUrl,
|
||||
GithubReaderProcessor,
|
||||
ProviderConfig,
|
||||
readConfig,
|
||||
} from './GithubReaderProcessor';
|
||||
|
||||
describe('GithubReaderProcessor', () => {
|
||||
describe('getApiRequestOptions', () => {
|
||||
it('sets the correct API version', () => {
|
||||
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
|
||||
expect((getApiRequestOptions(config).headers as any).Accept).toEqual(
|
||||
'application/vnd.github.v3.raw',
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: ProviderConfig = {
|
||||
target: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: ProviderConfig = {
|
||||
target: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getApiRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getApiRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRawRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: ProviderConfig = {
|
||||
target: '',
|
||||
rawBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: ProviderConfig = {
|
||||
target: '',
|
||||
rawBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getRawRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getRawRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApiUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
|
||||
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
|
||||
});
|
||||
|
||||
it('happy path for github', () => {
|
||||
const config: ProviderConfig = {
|
||||
target: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
};
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for ghe', () => {
|
||||
const config: ProviderConfig = {
|
||||
target: 'https://ghe.mycompany.net',
|
||||
apiBaseUrl: 'https://ghe.mycompany.net/api/v3',
|
||||
};
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRawUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
|
||||
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
|
||||
});
|
||||
|
||||
it('happy path for github', () => {
|
||||
const config: ProviderConfig = {
|
||||
target: 'https://github.com',
|
||||
rawBaseUrl: 'https://raw.githubusercontent.com',
|
||||
};
|
||||
expect(
|
||||
getRawUrl(
|
||||
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for ghe', () => {
|
||||
const config: ProviderConfig = {
|
||||
target: 'https://ghe.mycompany.net',
|
||||
rawBaseUrl: 'https://ghe.mycompany.net/raw',
|
||||
};
|
||||
expect(
|
||||
getRawUrl(
|
||||
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readConfig', () => {
|
||||
function config(
|
||||
providers: { target: string; apiBaseUrl?: string; token?: string }[],
|
||||
) {
|
||||
return ConfigReader.fromConfigs([
|
||||
{
|
||||
context: '',
|
||||
data: {
|
||||
catalog: { processors: { github: { providers } } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
it('adds a default GitHub entry when missing', () => {
|
||||
const output = readConfig(config([]), getVoidLogger());
|
||||
expect(output).toEqual([
|
||||
{
|
||||
target: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
rawBaseUrl: 'https://raw.githubusercontent.com',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the correct GitHub API base URL when missing', () => {
|
||||
const output = readConfig(
|
||||
config([{ target: 'https://github.com' }]),
|
||||
getVoidLogger(),
|
||||
);
|
||||
expect(output).toEqual([
|
||||
{
|
||||
target: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
rawBaseUrl: 'https://raw.githubusercontent.com',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects custom targets with no base URLs', () => {
|
||||
expect(() =>
|
||||
readConfig(
|
||||
config([{ target: 'https://ghe.company.com' }]),
|
||||
getVoidLogger(),
|
||||
),
|
||||
).toThrow(
|
||||
'Provider at https://ghe.company.com must configure an explicit apiBaseUrl or rawBaseUrl',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
expect(() =>
|
||||
readConfig(config([{ target: 7 } as any]), getVoidLogger()),
|
||||
).toThrow(/target/);
|
||||
expect(() =>
|
||||
readConfig(config([{ noTarget: '7' } as any]), getVoidLogger()),
|
||||
).toThrow(/target/);
|
||||
expect(() =>
|
||||
readConfig(
|
||||
config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
|
||||
getVoidLogger(),
|
||||
),
|
||||
).toThrow(/apiBaseUrl/);
|
||||
expect(() =>
|
||||
readConfig(
|
||||
config([{ target: 'https://github.com', token: 7 } as any]),
|
||||
getVoidLogger(),
|
||||
),
|
||||
).toThrow(/token/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('implementation', () => {
|
||||
it('rejects unknown types', async () => {
|
||||
const processor = new GithubReaderProcessor([
|
||||
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
|
||||
]);
|
||||
const location: LocationSpec = {
|
||||
type: 'not-github/api',
|
||||
target: 'https://github.com',
|
||||
};
|
||||
await expect(
|
||||
processor.readLocation(location, false, () => {}),
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
const processor = new GithubReaderProcessor([
|
||||
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
|
||||
]);
|
||||
const location: LocationSpec = {
|
||||
type: 'github/api',
|
||||
target: 'https://not.github.com/apa',
|
||||
};
|
||||
await expect(
|
||||
processor.readLocation(location, false, () => {}),
|
||||
).rejects.toThrow(
|
||||
/There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,33 +15,207 @@
|
||||
*/
|
||||
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import fetch, { RequestInit, HeadersInit } from 'node-fetch';
|
||||
import { Config } from '@backstage/config';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import fetch, { HeadersInit, RequestInit } from 'node-fetch';
|
||||
import { Logger } from 'winston';
|
||||
import * as result from './results';
|
||||
import { LocationProcessor, LocationProcessorEmit } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class GithubReaderProcessor implements LocationProcessor {
|
||||
private privateToken: string;
|
||||
/**
|
||||
* The configuration parameters for a single GitHub API provider.
|
||||
*/
|
||||
export type ProviderConfig = {
|
||||
/**
|
||||
* The prefix of the target that this matches on, e.g. "https://github.com",
|
||||
* with no trailing slash.
|
||||
*/
|
||||
target: string;
|
||||
|
||||
constructor(config?: Config) {
|
||||
this.privateToken =
|
||||
config?.getOptionalString('catalog.processors.github.privateToken') ?? '';
|
||||
/**
|
||||
* The base URL of the API of this provider, e.g. "https://api.github.com",
|
||||
* with no trailing slash.
|
||||
*
|
||||
* May be omitted specifically for GitHub; then it will be deduced.
|
||||
*
|
||||
* The API will always be preferred if both its base URL and a token are
|
||||
* present.
|
||||
*/
|
||||
apiBaseUrl?: string;
|
||||
|
||||
/**
|
||||
* The base URL of the raw fetch endpoint of this provider, e.g.
|
||||
* "https://raw.githubusercontent.com", with no trailing slash.
|
||||
*
|
||||
* May be omitted specifically for GitHub; then it will be deduced.
|
||||
*
|
||||
* The API will always be preferred if both its base URL and a token are
|
||||
* present.
|
||||
*/
|
||||
rawBaseUrl?: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests to this provider.
|
||||
*
|
||||
* If no token is specified, anonymous access is used.
|
||||
*/
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export function getApiRequestOptions(provider: ProviderConfig): RequestInit {
|
||||
const headers: HeadersInit = {
|
||||
Accept: 'application/vnd.github.v3.raw',
|
||||
};
|
||||
|
||||
if (provider.token) {
|
||||
headers.Authorization = `token ${provider.token}`;
|
||||
}
|
||||
|
||||
getRequestOptions(): RequestInit {
|
||||
const headers: HeadersInit = {
|
||||
Accept: 'application/vnd.github.v3.raw',
|
||||
};
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.privateToken !== '') {
|
||||
headers.Authorization = `token ${this.privateToken}`;
|
||||
export function getRawRequestOptions(provider: ProviderConfig): RequestInit {
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (provider.token) {
|
||||
headers.Authorization = `token ${provider.token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
// Converts for example
|
||||
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
|
||||
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
|
||||
export function getApiUrl(target: string, provider: ProviderConfig): URL {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
|
||||
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
!ref ||
|
||||
(filepathtype !== 'blob' && filepathtype !== 'raw') ||
|
||||
!filepath?.match(/\.ya?ml$/)
|
||||
) {
|
||||
throw new Error('Wrong URL or invalid file path');
|
||||
}
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
headers,
|
||||
};
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
return new URL(
|
||||
`${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
return requestOptions;
|
||||
// Converts for example
|
||||
// from: https://github.com/a/b/blob/branchname/c.yaml
|
||||
// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
|
||||
export function getRawUrl(target: string, provider: ProviderConfig): URL {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
|
||||
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
!ref ||
|
||||
(filepathtype !== 'blob' && filepathtype !== 'raw') ||
|
||||
!filepath?.match(/\.ya?ml$/)
|
||||
) {
|
||||
throw new Error('Wrong URL or invalid file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
return new URL(
|
||||
`${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function readConfig(config: Config, logger: Logger): ProviderConfig[] {
|
||||
const providers: ProviderConfig[] = [];
|
||||
|
||||
// TODO(freben): Deprecate the old config root entirely in a later release
|
||||
if (config.has('catalog.processors.githubApi')) {
|
||||
logger.warn(
|
||||
'The catalog.processors.githubApi configuration key has been deprecated, please use catalog.processors.github instead',
|
||||
);
|
||||
}
|
||||
|
||||
// In a previous version of the configuration, we only supported github,
|
||||
// and the "privateToken" key held the token to use for it. The new
|
||||
// configuration method is to use the "providers" key instead.
|
||||
const providerConfigs =
|
||||
config.getOptionalConfigArray('catalog.processors.github.providers') ??
|
||||
config.getOptionalConfigArray('catalog.processors.githubApi.providers') ??
|
||||
[];
|
||||
const legacyToken =
|
||||
config.getOptionalString('catalog.processors.github.privateToken') ??
|
||||
config.getOptionalString('catalog.processors.githubApi.privateToken');
|
||||
|
||||
// First read all the explicit providers
|
||||
for (const providerConfig of providerConfigs) {
|
||||
const target = providerConfig.getString('target').replace(/\/+$/, '');
|
||||
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
|
||||
let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl');
|
||||
const token = providerConfig.getOptionalString('token');
|
||||
|
||||
if (apiBaseUrl) {
|
||||
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
|
||||
} else if (target === 'https://github.com') {
|
||||
apiBaseUrl = 'https://api.github.com';
|
||||
}
|
||||
|
||||
if (rawBaseUrl) {
|
||||
rawBaseUrl = rawBaseUrl.replace(/\/+$/, '');
|
||||
} else if (target === 'https://github.com') {
|
||||
rawBaseUrl = 'https://raw.githubusercontent.com';
|
||||
}
|
||||
|
||||
if (!apiBaseUrl && !rawBaseUrl) {
|
||||
throw new Error(
|
||||
`Provider at ${target} must configure an explicit apiBaseUrl or rawBaseUrl`,
|
||||
);
|
||||
}
|
||||
|
||||
providers.push({ target, apiBaseUrl, rawBaseUrl, token });
|
||||
}
|
||||
|
||||
// If no explicit github.com provider was added, put one in the list as
|
||||
// a convenience
|
||||
if (!providers.some(p => p.target === 'https://github.com')) {
|
||||
providers.push({
|
||||
target: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
rawBaseUrl: 'https://raw.githubusercontent.com',
|
||||
token: legacyToken,
|
||||
});
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* A processor that adds the ability to read files from GitHub v3 APIs, such as
|
||||
* the one exposed by GitHub itself.
|
||||
*/
|
||||
export class GithubReaderProcessor implements LocationProcessor {
|
||||
private providers: ProviderConfig[];
|
||||
|
||||
static fromConfig(config: Config, logger: Logger) {
|
||||
return new GithubReaderProcessor(readConfig(config, logger));
|
||||
}
|
||||
|
||||
constructor(providers: ProviderConfig[]) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
@@ -49,16 +223,30 @@ export class GithubReaderProcessor implements LocationProcessor {
|
||||
optional: boolean,
|
||||
emit: LocationProcessorEmit,
|
||||
): Promise<boolean> {
|
||||
if (location.type !== 'github') {
|
||||
// The github/api type is for backward compatibility
|
||||
if (location.type !== 'github' && location.type !== 'github/api') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = this.buildRawUrl(location.target);
|
||||
const provider = this.providers.find(p =>
|
||||
location.target.startsWith(`${p.target}/`),
|
||||
);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.github.providers.`,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(freben): Should "hard" errors thrown by this line be treated as
|
||||
// notFound instead of fatal?
|
||||
const response = await fetch(url.toString(), this.getRequestOptions());
|
||||
try {
|
||||
const useApi =
|
||||
provider.apiBaseUrl && (provider.token || !provider.rawBaseUrl);
|
||||
const url = useApi
|
||||
? getApiUrl(location.target, provider)
|
||||
: getRawUrl(location.target, provider);
|
||||
const options = useApi
|
||||
? getApiRequestOptions(provider)
|
||||
: getRawRequestOptions(provider);
|
||||
const response = await fetch(url.toString(), options);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.buffer();
|
||||
@@ -80,41 +268,4 @@ export class GithubReaderProcessor implements LocationProcessor {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Converts
|
||||
// from: https://github.com/a/b/blob/master/c.yaml
|
||||
// to: https://raw.githubusercontent.com/a/b/master/c.yaml
|
||||
private buildRawUrl(target: string): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
repoName,
|
||||
blobKeyword,
|
||||
...restOfPath
|
||||
] = url.pathname.split('/');
|
||||
|
||||
if (
|
||||
url.hostname !== 'github.com' ||
|
||||
empty !== '' ||
|
||||
userOrOrg === '' ||
|
||||
repoName === '' ||
|
||||
blobKeyword !== 'blob' ||
|
||||
!restOfPath.join('/').match(/\.yaml$/)
|
||||
) {
|
||||
throw new Error('Wrong GitHub URL');
|
||||
}
|
||||
|
||||
// Removing the "blob" part
|
||||
url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/');
|
||||
url.hostname = 'raw.githubusercontent.com';
|
||||
url.protocol = 'https';
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect url: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
# Catalog GraphQL Plugin
|
||||
|
||||
## Getting Started
|
||||
|
||||
This is the Catalog GraphQL plugin.
|
||||
|
||||
It provides the `catalog` part of the GraphQL schema.
|
||||
|
||||
To register it with the GraphQL backend, be sure to follow the [Getting Started](../graphql/README.md#getting-started) guide of the GraphQL plugin.
|
||||
|
||||
<!-- TODO: Need to make the GraphQL plugin compatible with non forked repos >
|
||||
@@ -0,0 +1,15 @@
|
||||
overwrite: true
|
||||
generates:
|
||||
./src/graphql/types.ts:
|
||||
schema: ./src/schema.js
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-resolvers
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- eslint --fix
|
||||
config:
|
||||
contextType: '@graphql-modules/core#ModuleContext'
|
||||
allowParentTypeOverride: true
|
||||
useIndexSignature: true
|
||||
defaultMapper: Partial<{T}>
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-graphql",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli backend:dev",
|
||||
"build": "backstage-cli backend:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"generate:types": "graphql-codegen",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@graphql-modules/core": "^0.7.17",
|
||||
"apollo-server": "^2.16.1",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@graphql-codegen/cli": "^1.17.7",
|
||||
"@graphql-codegen/typescript": "^1.17.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^1.17.7",
|
||||
"ts-node": "^8.10.2",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"msw": "^0.19.5",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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 { createModule } from './module';
|
||||
import { execute } from 'graphql';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ReaderEntity } from '../service/client';
|
||||
import { createLogger } from 'winston';
|
||||
import { gql } from 'apollo-server';
|
||||
|
||||
describe('Catalog Module', () => {
|
||||
const worker = setupServer();
|
||||
const mockCatalogBaseUrl = 'http://im.mock';
|
||||
const mockConfig = ConfigReader.fromConfigs([
|
||||
{
|
||||
context: '',
|
||||
data: {
|
||||
backend: {
|
||||
baseUrl: mockCatalogBaseUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
beforeAll(() => worker.listen());
|
||||
afterAll(() => worker.close());
|
||||
|
||||
afterEach(() => worker.resetHandlers());
|
||||
|
||||
describe('Default Entity', () => {
|
||||
beforeEach(() => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: {},
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the catalog client when requesting entities', async () => {
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
kind
|
||||
apiVersion
|
||||
metadata {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.kind).toBe('Component');
|
||||
expect(catalogItem.apiVersion).toBe('something');
|
||||
expect(catalogItem.metadata.name).toBe('Ben');
|
||||
});
|
||||
|
||||
it('Defaults to empty annotations when none are provided', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: null as any,
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: {},
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
annotations
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.annotations).toEqual({});
|
||||
});
|
||||
|
||||
it('Defaults to empty labels when none are provided', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: null as any,
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
labels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.labels).toEqual({});
|
||||
});
|
||||
it('Returns the correct record from the annotation dictionary', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: { lob: 'bloben' },
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: {},
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
test: annotation(name: "lob")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.test).toEqual('bloben');
|
||||
});
|
||||
it('Returns the correct record from the labels dictionary', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: { lob2: 'bloben' },
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
test: label(name: "lob2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.test).toEqual('bloben');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 { Logger } from 'winston';
|
||||
import { makeExecutableSchema } from 'apollo-server';
|
||||
import { GraphQLModule } from '@graphql-modules/core';
|
||||
import { Resolvers, CatalogQuery } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { CatalogClient } from '../service/client';
|
||||
import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export interface ModuleOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export async function createModule(
|
||||
options: ModuleOptions,
|
||||
): Promise<GraphQLModule> {
|
||||
const { default: typeDefs } = require('../schema');
|
||||
|
||||
const catalogClient = new CatalogClient(
|
||||
options.config.getString('backend.baseUrl'),
|
||||
);
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
JSON: GraphQLJSON,
|
||||
JSONObject: GraphQLJSONObject,
|
||||
DefaultEntitySpec: {
|
||||
raw: rootValue => {
|
||||
const { entity } = rootValue as { entity: Entity };
|
||||
return entity.spec ?? null;
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
catalog: () => ({} as CatalogQuery),
|
||||
},
|
||||
CatalogQuery: {
|
||||
list: async () => {
|
||||
return await catalogClient.list();
|
||||
},
|
||||
},
|
||||
CatalogEntity: {
|
||||
metadata: entity => ({ ...entity.metadata!, entity }),
|
||||
spec: entity => ({ ...entity.spec!, entity }),
|
||||
},
|
||||
EntityMetadata: {
|
||||
__resolveType: rootValue => {
|
||||
const {
|
||||
entity: { kind },
|
||||
} = rootValue as { entity: Entity };
|
||||
switch (kind) {
|
||||
case 'Component':
|
||||
return 'ComponentMetadata';
|
||||
case 'Template':
|
||||
return 'TemplateMetadata';
|
||||
default:
|
||||
return 'DefaultEntityMetadata';
|
||||
}
|
||||
},
|
||||
annotation: (e, { name }) => e.annotations?.[name] ?? null,
|
||||
labels: e => e.labels ?? {},
|
||||
annotations: e => e.annotations ?? {},
|
||||
label: (e, { name }) => e.labels?.[name] ?? null,
|
||||
},
|
||||
EntitySpec: {
|
||||
__resolveType: rootValue => {
|
||||
const {
|
||||
entity: { kind },
|
||||
} = rootValue as { entity: Entity };
|
||||
|
||||
switch (kind) {
|
||||
case 'Component':
|
||||
return 'ComponentEntitySpec';
|
||||
case 'Template':
|
||||
return 'TemplateEntitySpec';
|
||||
default:
|
||||
return 'DefaultEntitySpec';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
inheritResolversFromInterfaces: true,
|
||||
});
|
||||
|
||||
const module = new GraphQLModule({
|
||||
extraSchemas: [schema],
|
||||
logger: options.logger as any,
|
||||
});
|
||||
|
||||
return module;
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
/*
|
||||
* 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 {
|
||||
GraphQLResolveInfo,
|
||||
GraphQLScalarType,
|
||||
GraphQLScalarTypeConfig,
|
||||
} from 'graphql';
|
||||
import { ModuleContext } from '@graphql-modules/core';
|
||||
|
||||
export type Maybe<T> = T | null;
|
||||
export type Exact<T extends { [key: string]: unknown }> = {
|
||||
[K in keyof T]: T[K];
|
||||
};
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type RequireFields<T, K extends keyof T> = {
|
||||
[X in Exclude<keyof T, K>]?: T[X];
|
||||
} &
|
||||
{ [P in K]-?: NonNullable<T[P]> };
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
JSON: any;
|
||||
JSONObject: any;
|
||||
};
|
||||
|
||||
export type EntityMetadata = {
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type EntityMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type EntityMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DefaultEntityMetadata = EntityMetadata & {
|
||||
__typename?: 'DefaultEntityMetadata';
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type DefaultEntityMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DefaultEntityMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComponentMetadata = EntityMetadata & {
|
||||
__typename?: 'ComponentMetadata';
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
relationships?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ComponentMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComponentMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TemplateMetadata = EntityMetadata & {
|
||||
__typename?: 'TemplateMetadata';
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
updatedBy?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type TemplateMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TemplateMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TemplateEntitySpec = {
|
||||
__typename?: 'TemplateEntitySpec';
|
||||
type: Scalars['String'];
|
||||
path?: Maybe<Scalars['String']>;
|
||||
schema: Scalars['JSONObject'];
|
||||
templater: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComponentEntitySpec = {
|
||||
__typename?: 'ComponentEntitySpec';
|
||||
title: Scalars['String'];
|
||||
lifecycle: Scalars['String'];
|
||||
owner: Scalars['String'];
|
||||
};
|
||||
|
||||
export type LocationEntitySpec = {
|
||||
__typename?: 'LocationEntitySpec';
|
||||
type: Scalars['String'];
|
||||
target?: Maybe<Scalars['String']>;
|
||||
targets: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type DefaultEntitySpec = {
|
||||
__typename?: 'DefaultEntitySpec';
|
||||
raw?: Maybe<Scalars['JSONObject']>;
|
||||
};
|
||||
|
||||
export type EntitySpec =
|
||||
| DefaultEntitySpec
|
||||
| TemplateEntitySpec
|
||||
| LocationEntitySpec
|
||||
| ComponentEntitySpec;
|
||||
|
||||
export type CatalogEntity = {
|
||||
__typename?: 'CatalogEntity';
|
||||
apiVersion: Scalars['String'];
|
||||
kind: Scalars['String'];
|
||||
metadata?: Maybe<EntityMetadata>;
|
||||
spec: EntitySpec;
|
||||
};
|
||||
|
||||
export type CatalogQuery = {
|
||||
__typename?: 'CatalogQuery';
|
||||
list: Array<CatalogEntity>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
catalog: CatalogQuery;
|
||||
};
|
||||
|
||||
export type WithIndex<TObject> = TObject & Record<string, any>;
|
||||
export type ResolversObject<TObject> = WithIndex<TObject>;
|
||||
|
||||
export type ResolverTypeWrapper<T> = Promise<T> | T;
|
||||
|
||||
export type LegacyStitchingResolver<TResult, TParent, TContext, TArgs> = {
|
||||
fragment: string;
|
||||
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
||||
};
|
||||
|
||||
export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
|
||||
selectionSet: string;
|
||||
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
||||
};
|
||||
export type StitchingResolver<TResult, TParent, TContext, TArgs> =
|
||||
| LegacyStitchingResolver<TResult, TParent, TContext, TArgs>
|
||||
| NewStitchingResolver<TResult, TParent, TContext, TArgs>;
|
||||
export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =
|
||||
| ResolverFn<TResult, TParent, TContext, TArgs>
|
||||
| StitchingResolver<TResult, TParent, TContext, TArgs>;
|
||||
|
||||
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => Promise<TResult> | TResult;
|
||||
|
||||
export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>;
|
||||
|
||||
export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => TResult | Promise<TResult>;
|
||||
|
||||
export interface SubscriptionSubscriberObject<
|
||||
TResult,
|
||||
TKey extends string,
|
||||
TParent,
|
||||
TContext,
|
||||
TArgs
|
||||
> {
|
||||
subscribe: SubscriptionSubscribeFn<
|
||||
{ [key in TKey]: TResult },
|
||||
TParent,
|
||||
TContext,
|
||||
TArgs
|
||||
>;
|
||||
resolve?: SubscriptionResolveFn<
|
||||
TResult,
|
||||
{ [key in TKey]: TResult },
|
||||
TContext,
|
||||
TArgs
|
||||
>;
|
||||
}
|
||||
|
||||
export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
|
||||
subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
|
||||
resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
|
||||
}
|
||||
|
||||
export type SubscriptionObject<
|
||||
TResult,
|
||||
TKey extends string,
|
||||
TParent,
|
||||
TContext,
|
||||
TArgs
|
||||
> =
|
||||
| SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
|
||||
| SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
|
||||
|
||||
export type SubscriptionResolver<
|
||||
TResult,
|
||||
TKey extends string,
|
||||
TParent = {},
|
||||
TContext = {},
|
||||
TArgs = {}
|
||||
> =
|
||||
| ((
|
||||
...args: any[]
|
||||
) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
|
||||
| SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
|
||||
|
||||
export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
|
||||
parent: TParent,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => Maybe<TTypes> | Promise<Maybe<TTypes>>;
|
||||
|
||||
export type IsTypeOfResolverFn<T = {}> = (
|
||||
obj: T,
|
||||
info: GraphQLResolveInfo,
|
||||
) => boolean | Promise<boolean>;
|
||||
|
||||
export type NextResolverFn<T> = () => Promise<T>;
|
||||
|
||||
export type DirectiveResolverFn<
|
||||
TResult = {},
|
||||
TParent = {},
|
||||
TContext = {},
|
||||
TArgs = {}
|
||||
> = (
|
||||
next: NextResolverFn<TResult>,
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => TResult | Promise<TResult>;
|
||||
|
||||
/** Mapping between all available schema types and the resolvers types */
|
||||
export type ResolversTypes = ResolversObject<{
|
||||
JSON: ResolverTypeWrapper<Partial<Scalars['JSON']>>;
|
||||
JSONObject: ResolverTypeWrapper<Partial<Scalars['JSONObject']>>;
|
||||
EntityMetadata:
|
||||
| ResolversTypes['DefaultEntityMetadata']
|
||||
| ResolversTypes['ComponentMetadata']
|
||||
| ResolversTypes['TemplateMetadata'];
|
||||
String: ResolverTypeWrapper<Partial<Scalars['String']>>;
|
||||
Int: ResolverTypeWrapper<Partial<Scalars['Int']>>;
|
||||
DefaultEntityMetadata: ResolverTypeWrapper<Partial<DefaultEntityMetadata>>;
|
||||
ComponentMetadata: ResolverTypeWrapper<Partial<ComponentMetadata>>;
|
||||
TemplateMetadata: ResolverTypeWrapper<Partial<TemplateMetadata>>;
|
||||
TemplateEntitySpec: ResolverTypeWrapper<Partial<TemplateEntitySpec>>;
|
||||
ComponentEntitySpec: ResolverTypeWrapper<Partial<ComponentEntitySpec>>;
|
||||
LocationEntitySpec: ResolverTypeWrapper<Partial<LocationEntitySpec>>;
|
||||
DefaultEntitySpec: ResolverTypeWrapper<Partial<DefaultEntitySpec>>;
|
||||
EntitySpec: Partial<
|
||||
| ResolversTypes['DefaultEntitySpec']
|
||||
| ResolversTypes['TemplateEntitySpec']
|
||||
| ResolversTypes['LocationEntitySpec']
|
||||
| ResolversTypes['ComponentEntitySpec']
|
||||
>;
|
||||
CatalogEntity: ResolverTypeWrapper<
|
||||
Partial<
|
||||
Omit<CatalogEntity, 'spec'> & { spec: ResolversTypes['EntitySpec'] }
|
||||
>
|
||||
>;
|
||||
CatalogQuery: ResolverTypeWrapper<Partial<CatalogQuery>>;
|
||||
Query: ResolverTypeWrapper<{}>;
|
||||
Boolean: ResolverTypeWrapper<Partial<Scalars['Boolean']>>;
|
||||
}>;
|
||||
|
||||
/** Mapping between all available schema types and the resolvers parents */
|
||||
export type ResolversParentTypes = ResolversObject<{
|
||||
JSON: Partial<Scalars['JSON']>;
|
||||
JSONObject: Partial<Scalars['JSONObject']>;
|
||||
EntityMetadata:
|
||||
| ResolversParentTypes['DefaultEntityMetadata']
|
||||
| ResolversParentTypes['ComponentMetadata']
|
||||
| ResolversParentTypes['TemplateMetadata'];
|
||||
String: Partial<Scalars['String']>;
|
||||
Int: Partial<Scalars['Int']>;
|
||||
DefaultEntityMetadata: Partial<DefaultEntityMetadata>;
|
||||
ComponentMetadata: Partial<ComponentMetadata>;
|
||||
TemplateMetadata: Partial<TemplateMetadata>;
|
||||
TemplateEntitySpec: Partial<TemplateEntitySpec>;
|
||||
ComponentEntitySpec: Partial<ComponentEntitySpec>;
|
||||
LocationEntitySpec: Partial<LocationEntitySpec>;
|
||||
DefaultEntitySpec: Partial<DefaultEntitySpec>;
|
||||
EntitySpec: Partial<
|
||||
| ResolversParentTypes['DefaultEntitySpec']
|
||||
| ResolversParentTypes['TemplateEntitySpec']
|
||||
| ResolversParentTypes['LocationEntitySpec']
|
||||
| ResolversParentTypes['ComponentEntitySpec']
|
||||
>;
|
||||
CatalogEntity: Partial<
|
||||
Omit<CatalogEntity, 'spec'> & { spec: ResolversParentTypes['EntitySpec'] }
|
||||
>;
|
||||
CatalogQuery: Partial<CatalogQuery>;
|
||||
Query: {};
|
||||
Boolean: Partial<Scalars['Boolean']>;
|
||||
}>;
|
||||
|
||||
export interface JsonScalarConfig
|
||||
extends GraphQLScalarTypeConfig<ResolversTypes['JSON'], any> {
|
||||
name: 'JSON';
|
||||
}
|
||||
|
||||
export interface JsonObjectScalarConfig
|
||||
extends GraphQLScalarTypeConfig<ResolversTypes['JSONObject'], any> {
|
||||
name: 'JSONObject';
|
||||
}
|
||||
|
||||
export type EntityMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['EntityMetadata']
|
||||
> = ResolversObject<{
|
||||
__resolveType: TypeResolveFn<
|
||||
'DefaultEntityMetadata' | 'ComponentMetadata' | 'TemplateMetadata',
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<EntityMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<EntityMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type DefaultEntityMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['DefaultEntityMetadata']
|
||||
> = ResolversObject<{
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<DefaultEntityMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<DefaultEntityMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type ComponentMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['ComponentMetadata']
|
||||
> = ResolversObject<{
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<ComponentMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<ComponentMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
relationships?: Resolver<
|
||||
Maybe<ResolversTypes['String']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type TemplateMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['TemplateMetadata']
|
||||
> = ResolversObject<{
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<TemplateMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<TemplateMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
updatedBy?: Resolver<
|
||||
Maybe<ResolversTypes['String']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type TemplateEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['TemplateEntitySpec']
|
||||
> = ResolversObject<{
|
||||
type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
path?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
schema?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
templater?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type ComponentEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['ComponentEntitySpec']
|
||||
> = ResolversObject<{
|
||||
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
lifecycle?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
owner?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type LocationEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['LocationEntitySpec']
|
||||
> = ResolversObject<{
|
||||
type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
target?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
targets?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type DefaultEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['DefaultEntitySpec']
|
||||
> = ResolversObject<{
|
||||
raw?: Resolver<Maybe<ResolversTypes['JSONObject']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type EntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['EntitySpec']
|
||||
> = ResolversObject<{
|
||||
__resolveType: TypeResolveFn<
|
||||
| 'DefaultEntitySpec'
|
||||
| 'TemplateEntitySpec'
|
||||
| 'LocationEntitySpec'
|
||||
| 'ComponentEntitySpec',
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
}>;
|
||||
|
||||
export type CatalogEntityResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['CatalogEntity']
|
||||
> = ResolversObject<{
|
||||
apiVersion?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
kind?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
metadata?: Resolver<
|
||||
Maybe<ResolversTypes['EntityMetadata']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
spec?: Resolver<ResolversTypes['EntitySpec'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type CatalogQueryResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['CatalogQuery']
|
||||
> = ResolversObject<{
|
||||
list?: Resolver<
|
||||
Array<ResolversTypes['CatalogEntity']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type QueryResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['Query']
|
||||
> = ResolversObject<{
|
||||
catalog?: Resolver<ResolversTypes['CatalogQuery'], ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type Resolvers<ContextType = ModuleContext> = ResolversObject<{
|
||||
JSON?: GraphQLScalarType;
|
||||
JSONObject?: GraphQLScalarType;
|
||||
EntityMetadata?: EntityMetadataResolvers<ContextType>;
|
||||
DefaultEntityMetadata?: DefaultEntityMetadataResolvers<ContextType>;
|
||||
ComponentMetadata?: ComponentMetadataResolvers<ContextType>;
|
||||
TemplateMetadata?: TemplateMetadataResolvers<ContextType>;
|
||||
TemplateEntitySpec?: TemplateEntitySpecResolvers<ContextType>;
|
||||
ComponentEntitySpec?: ComponentEntitySpecResolvers<ContextType>;
|
||||
LocationEntitySpec?: LocationEntitySpecResolvers<ContextType>;
|
||||
DefaultEntitySpec?: DefaultEntitySpecResolvers<ContextType>;
|
||||
EntitySpec?: EntitySpecResolvers<ContextType>;
|
||||
CatalogEntity?: CatalogEntityResolvers<ContextType>;
|
||||
CatalogQuery?: CatalogQueryResolvers<ContextType>;
|
||||
Query?: QueryResolvers<ContextType>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config.
|
||||
*/
|
||||
export type IResolvers<ContextType = ModuleContext> = Resolvers<ContextType>;
|
||||
@@ -14,20 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
export * from './graphql/module';
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const metadataFields = /* GraphQL */ `
|
||||
name: String!
|
||||
annotations: JSONObject!
|
||||
annotation(name: String!): JSON
|
||||
labels: JSONObject!
|
||||
label(name: String!): JSON
|
||||
uid: String!
|
||||
etag: String!
|
||||
generation: Int!
|
||||
`;
|
||||
|
||||
const schema = /* GraphQL */ `
|
||||
scalar JSON
|
||||
scalar JSONObject
|
||||
|
||||
interface EntityMetadata {
|
||||
${metadataFields}
|
||||
}
|
||||
|
||||
type DefaultEntityMetadata implements EntityMetadata {
|
||||
${metadataFields}
|
||||
}
|
||||
|
||||
type ComponentMetadata implements EntityMetadata {
|
||||
${metadataFields}
|
||||
# mock field to prove extensions working
|
||||
relationships: String
|
||||
}
|
||||
|
||||
type TemplateMetadata implements EntityMetadata {
|
||||
${metadataFields}
|
||||
# mock field to prove extensions working
|
||||
updatedBy: String
|
||||
}
|
||||
|
||||
# TODO: move this definition into plugin-scaffolder-graphql
|
||||
type TemplateEntitySpec {
|
||||
type: String!
|
||||
path: String
|
||||
schema: JSONObject!
|
||||
templater: String!
|
||||
}
|
||||
|
||||
type ComponentEntitySpec {
|
||||
type: String!
|
||||
lifecycle: String!
|
||||
owner: String!
|
||||
}
|
||||
|
||||
type DefaultEntitySpec {
|
||||
raw: JSONObject
|
||||
}
|
||||
|
||||
union EntitySpec = DefaultEntitySpec | TemplateEntitySpec | ComponentEntitySpec
|
||||
|
||||
type CatalogEntity {
|
||||
apiVersion: String!
|
||||
kind: String!
|
||||
metadata: EntityMetadata
|
||||
spec: EntitySpec!
|
||||
}
|
||||
|
||||
type CatalogQuery {
|
||||
list: [CatalogEntity!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
catalog: CatalogQuery!
|
||||
}
|
||||
`;
|
||||
|
||||
export default schema;
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 { CatalogClient } from './client';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
describe('Catalog GraphQL Module', () => {
|
||||
const worker = setupServer();
|
||||
|
||||
beforeAll(() => worker.listen());
|
||||
afterAll(() => worker.close());
|
||||
|
||||
afterEach(() => worker.resetHandlers());
|
||||
|
||||
const baseUrl = 'http://localhost:1234';
|
||||
|
||||
it('will return the entities', async () => {
|
||||
const expectedResponse = [{ id: 'something' }];
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(expectedResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const client = new CatalogClient(baseUrl);
|
||||
|
||||
const response = await client.list();
|
||||
|
||||
expect(response).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('throws an error with the text', async () => {
|
||||
const expectedResponse = 'something broke';
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text(expectedResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const client = new CatalogClient(baseUrl);
|
||||
|
||||
await expect(() => client.list()).rejects.toThrow(expectedResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { Entity, EntityMeta } from '@backstage/catalog-model';
|
||||
import fetch from 'node-fetch';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
|
||||
export interface ReaderEntityMeta extends EntityMeta {
|
||||
uid: string;
|
||||
etag: string;
|
||||
generation: number;
|
||||
namespace: string;
|
||||
annotations: Record<string, string>;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
export interface ReaderEntity extends Entity {
|
||||
metadata: JsonObject & ReaderEntityMeta;
|
||||
}
|
||||
export class CatalogClient {
|
||||
constructor(private baseUrl: string) {}
|
||||
async list(): Promise<ReaderEntity[]> {
|
||||
const res = await fetch(`${this.baseUrl}/catalog/entities`);
|
||||
if (!res.ok) {
|
||||
// todo(blam): need some better way to handle errors here
|
||||
// experiment with throwing the input errors etc and having graphql versions of that
|
||||
throw new Error(await res.text());
|
||||
}
|
||||
|
||||
const entities: ReaderEntity[] = await res.json();
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
export {};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,14 +21,15 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-techdocs": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react": "^16.9",
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
@@ -37,13 +38,12 @@
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3",
|
||||
"swr": "^0.3.0",
|
||||
"@types/react": "^16.9"
|
||||
"swr": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
|
||||
@@ -47,7 +47,7 @@ export const EntityFilterGroupsProvider = ({
|
||||
function useProvideEntityFilters(): FilterGroupsContext {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [{ value: entities, error }, doReload] = useAsyncFn(() =>
|
||||
catalogApi.getEntities(),
|
||||
catalogApi.getEntities({ kind: 'Component' }),
|
||||
);
|
||||
|
||||
const filterGroups = useRef<{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-circleci",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"postpack": "backstage-cli postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -38,8 +38,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-cloudbuild",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,11 +21,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/core-api": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/core-api": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -40,8 +40,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-explore",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,21 +21,21 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^15.3.3",
|
||||
"react-router": "6.0.0-beta.0"
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-gcp-projects",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,8 +21,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -32,8 +32,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* 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 { GCPApi } from './GCPApi';
|
||||
import { Project, Operation, Status } from './types';
|
||||
|
||||
const BaseURL =
|
||||
'https://content-cloudresourcemanager.googleapis.com/v1/projects';
|
||||
|
||||
export class GCPClient implements GCPApi {
|
||||
async listProjects({ token }: { token: string }): Promise<Project[]> {
|
||||
const response = await fetch(BaseURL, {
|
||||
headers: new Headers({
|
||||
Accept: '*/*',
|
||||
Authorization: `Bearer ${token}`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return [
|
||||
{
|
||||
name: 'Error',
|
||||
projectNumber: 'Response status is not OK',
|
||||
projectId: 'Error',
|
||||
lifecycleState: 'error',
|
||||
createTime: 'Error',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return data.projects;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getProject(
|
||||
projectId: string,
|
||||
token: Promise<string>,
|
||||
): Promise<Project> {
|
||||
const url = `${BaseURL}/${projectId}`;
|
||||
const response = await fetch(url, {
|
||||
headers: new Headers({
|
||||
Authorization: `Bearer ${await token}`,
|
||||
}),
|
||||
});
|
||||
|
||||
const dataBlank: Project = {
|
||||
name: 'Error',
|
||||
projectNumber: `Response status is ${response.status}`,
|
||||
projectId: 'Error',
|
||||
lifecycleState: 'error',
|
||||
createTime: 'Error',
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
return dataBlank;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const newData: Project = data;
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
async createProject(
|
||||
projectName: string,
|
||||
projectId: string,
|
||||
token: string,
|
||||
): Promise<Operation> {
|
||||
const status: Status = {
|
||||
code: 0,
|
||||
message: '',
|
||||
details: [],
|
||||
};
|
||||
|
||||
const op: Operation = {
|
||||
name: '',
|
||||
metadata: '',
|
||||
done: true,
|
||||
error: status,
|
||||
response: '',
|
||||
};
|
||||
|
||||
const newProject: Project = {
|
||||
name: projectName,
|
||||
projectId: projectId,
|
||||
};
|
||||
|
||||
const body = JSON.stringify(newProject);
|
||||
|
||||
const response = await fetch(BaseURL, {
|
||||
headers: new Headers({
|
||||
Accept: '*/*',
|
||||
Authorization: `Bearer ${token}`,
|
||||
}),
|
||||
body: body,
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
status.code = response.status;
|
||||
return op;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,16 @@
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { Project, Operation } from './types';
|
||||
|
||||
export const GCPApiRef = createApiRef<GCPApi>({
|
||||
export const gcpApiRef = createApiRef<GcpApi>({
|
||||
id: 'plugin.gcpprojects.service',
|
||||
description: 'Used by the GCP Projects plugin to make requests',
|
||||
});
|
||||
|
||||
export type GCPApi = {
|
||||
listProjects: ({ token }: { token: string }) => Promise<Project[]>;
|
||||
getProject: (projectId: string, token: Promise<string>) => Promise<Project>;
|
||||
createProject: (
|
||||
projectName: string,
|
||||
projectId: string,
|
||||
owner: string,
|
||||
token: string,
|
||||
) => Promise<Operation>;
|
||||
export type GcpApi = {
|
||||
listProjects(): Promise<Project[]>;
|
||||
getProject(projectId: string): Promise<Project>;
|
||||
createProject(options: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}): Promise<Operation>;
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { OAuthApi } from '@backstage/core';
|
||||
import { GcpApi } from './GcpApi';
|
||||
import { Operation, Project } from './types';
|
||||
|
||||
const BASE_URL =
|
||||
'https://content-cloudresourcemanager.googleapis.com/v1/projects';
|
||||
|
||||
export class GcpClient implements GcpApi {
|
||||
constructor(private readonly googleAuthApi: OAuthApi) {}
|
||||
|
||||
async listProjects(): Promise<Project[]> {
|
||||
const response = await fetch(BASE_URL, {
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
Authorization: `Bearer ${await this.getToken()}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`List request failed to ${BASE_URL} with ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { projects } = await response.json();
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProject(projectId: string): Promise<Project> {
|
||||
const url = `${BASE_URL}/${projectId}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.getToken()}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Get request failed to ${url} with ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async createProject(options: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}): Promise<Operation> {
|
||||
const newProject: Project = {
|
||||
name: options.projectName,
|
||||
projectId: options.projectId,
|
||||
};
|
||||
|
||||
const response = await fetch(BASE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
Authorization: `Bearer ${await this.getToken()}`,
|
||||
},
|
||||
body: JSON.stringify(newProject),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Create request failed to ${BASE_URL} with ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getToken(): Promise<string> {
|
||||
// NOTE(freben): There's a .read-only variant of this scope that we could
|
||||
// use for readonly operations, but that means we would ask the user for a
|
||||
// second auth during creation and I decided to keep the wider scope for
|
||||
// all ops for now
|
||||
return this.googleAuthApi.getAccessToken(
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './GCPApi';
|
||||
export * from './GCPClient';
|
||||
export * from './GcpApi';
|
||||
export * from './GcpClient';
|
||||
export * from './types';
|
||||
|
||||
@@ -14,24 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Grid, Button, TextField } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
InfoCard,
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
InfoCard,
|
||||
Page,
|
||||
pageTheme,
|
||||
SimpleStepper,
|
||||
SimpleStepperStep,
|
||||
StructuredMetadataTable,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
Header,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import { Button, Grid, TextField } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const Project: FC<{}> = () => {
|
||||
export const Project = () => {
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [projectId, setProjectId] = useState('');
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
useApi,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -27,20 +38,9 @@ import {
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
useApi,
|
||||
googleAuthApiRef,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
Header,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
Content,
|
||||
ContentHeader,
|
||||
} from '@backstage/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { GCPApiRef } from '../../api';
|
||||
import { gcpApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
@@ -56,34 +56,27 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
}));
|
||||
|
||||
const DetailsPage = () => {
|
||||
const api = useApi(GCPApiRef);
|
||||
const googleApi = useApi(googleAuthApiRef);
|
||||
const token = googleApi.getAccessToken(
|
||||
'https://www.googleapis.com/auth/cloud-platform.read-only',
|
||||
);
|
||||
|
||||
const api = useApi(gcpApiRef);
|
||||
const classes = useStyles();
|
||||
const status = useAsync(
|
||||
() =>
|
||||
|
||||
const { loading, error, value: details } = useAsync(
|
||||
async () =>
|
||||
api.getProject(
|
||||
decodeURIComponent(location.search.split('projectId=')[1]),
|
||||
token,
|
||||
),
|
||||
[location.search],
|
||||
);
|
||||
|
||||
if (status.loading) {
|
||||
if (loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
} else if (error) {
|
||||
return (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {status.error.message}
|
||||
</Typography>
|
||||
<WarningPanel title="Failed to load project">
|
||||
{error.toString()}
|
||||
</WarningPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const details = status.value;
|
||||
|
||||
return (
|
||||
<Table component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
// NEEDS WORK
|
||||
|
||||
import {
|
||||
Link,
|
||||
useApi,
|
||||
googleAuthApiRef,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
Header,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Link,
|
||||
Page,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
useApi,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
Button,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Table,
|
||||
@@ -38,11 +39,10 @@ import {
|
||||
TableRow,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { GCPApiRef, Project } from '../../api';
|
||||
import { gcpApiRef, Project } from '../../api';
|
||||
|
||||
const LongText = ({ text, max }: { text: string; max: number }) => {
|
||||
if (text.length < max) {
|
||||
@@ -63,27 +63,17 @@ const labels = (
|
||||
);
|
||||
|
||||
const PageContents = () => {
|
||||
const api = useApi(GCPApiRef);
|
||||
const googleApi = useApi(googleAuthApiRef);
|
||||
const api = useApi(gcpApiRef);
|
||||
|
||||
const { loading, error, value } = useAsync(async () => {
|
||||
const token = await googleApi.getAccessToken(
|
||||
'https://www.googleapis.com/auth/cloud-platform.read-only',
|
||||
);
|
||||
|
||||
const projects = api.listProjects({ token });
|
||||
return projects;
|
||||
});
|
||||
const { loading, error, value } = useAsync(() => api.listProjects());
|
||||
|
||||
if (loading) {
|
||||
return <LinearProgress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
} else if (error) {
|
||||
return (
|
||||
<Typography variant="h2" color="error">
|
||||
{error.message}{' '}
|
||||
</Typography>
|
||||
<WarningPanel title="Failed to load projects">
|
||||
{error.toString()}
|
||||
</WarningPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,34 +15,43 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createApiFactory,
|
||||
googleAuthApiRef,
|
||||
} from '@backstage/core';
|
||||
import { ProjectListPage } from './components/ProjectListPage';
|
||||
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
|
||||
import { gcpApiRef, GcpClient } from './api';
|
||||
import { NewProjectPage } from './components/NewProjectPage';
|
||||
import { GCPApiRef, GCPClient } from './api';
|
||||
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
|
||||
import { ProjectListPage } from './components/ProjectListPage';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/gcp-projects',
|
||||
title: 'GCP Projects',
|
||||
});
|
||||
export const ProjectRouteRef = createRouteRef({
|
||||
export const projectRouteRef = createRouteRef({
|
||||
path: '/gcp-projects/project',
|
||||
title: 'GCP Project Page',
|
||||
});
|
||||
export const NewProjectRouteRef = createRouteRef({
|
||||
export const newProjectRouteRef = createRouteRef({
|
||||
path: '/gcp-projects/new',
|
||||
title: 'GCP Project Page',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'gcp-projects',
|
||||
apis: [createApiFactory(GCPApiRef, new GCPClient())],
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: gcpApiRef,
|
||||
deps: { googleAuthApi: googleAuthApiRef },
|
||||
factory({ googleAuthApi }) {
|
||||
return new GcpClient(googleAuthApi);
|
||||
},
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, ProjectListPage);
|
||||
router.addRoute(ProjectRouteRef, ProjectDetailsPage);
|
||||
router.addRoute(NewProjectRouteRef, NewProjectPage);
|
||||
router.addRoute(projectRouteRef, ProjectDetailsPage);
|
||||
router.addRoute(newProjectRouteRef, NewProjectPage);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-github-actions",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,11 +21,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/core-api": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/core-api": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -40,8 +40,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-gitops-profiles",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,8 +21,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -32,8 +32,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphiql",
|
||||
"description": "Backstage plugin for browsing GraphQL APIs",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -31,8 +31,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -43,9 +43,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Getting Started
|
||||
|
||||
This backend plugin can be started in a standalone mode from directly in this package
|
||||
with `yarn start`. However, it will have limited functionality and that process is
|
||||
most convenient when developing the plugin itself.
|
||||
This is the GraphQL Backend plugin.
|
||||
|
||||
It is responsible for merging different `graphql-plugins` together to provide the end schema.
|
||||
|
||||
To run it within the backend do:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphql-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -19,19 +19,24 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog-graphql": "^0.1.1-alpha.22",
|
||||
"@graphql-modules/core": "^0.7.17",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"apollo-server": "^2.16.0",
|
||||
"apollo-server-express": "^2.16.0",
|
||||
"apollo-server": "^2.16.1",
|
||||
"apollo-server-express": "^2.16.1",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"graphql": "^15.3.0",
|
||||
"whatwg-fetch": "^3.4.0",
|
||||
"helmet": "^4.0.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"msw": "^0.20.5",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
type CatalogEntity {
|
||||
id: String
|
||||
}
|
||||
|
||||
type CatalogQuery {
|
||||
list: [CatalogEntity!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
catalog: CatalogQuery!
|
||||
}
|
||||
|
||||
@@ -14,6 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
require('whatwg-fetch');
|
||||
|
||||
export * from './service/router';
|
||||
|
||||
@@ -13,10 +13,25 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import './router';
|
||||
import { createRouter } from './router';
|
||||
import supertest from 'supertest';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createLogger } from 'winston';
|
||||
import express from 'express';
|
||||
|
||||
describe('Router', () => {
|
||||
it('should pass the test', () => {
|
||||
expect(true).toBeTruthy();
|
||||
describe('/health', () => {
|
||||
it('should return ok', async () => {
|
||||
const config = ConfigReader.fromConfigs([
|
||||
{ data: { backend: { baseUrl: 'lol' } }, context: 'something' },
|
||||
]);
|
||||
|
||||
const router = await createRouter({ config, logger: createLogger() });
|
||||
const app = express().use(router);
|
||||
|
||||
const { body } = await supertest(app).get('/health');
|
||||
|
||||
expect(body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,11 @@ import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs';
|
||||
import { GraphQLModule } from '@graphql-modules/core';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { createModule as createCatalogModule } from '@backstage/plugin-catalog-graphql';
|
||||
import { Config } from '@backstage/config';
|
||||
import helmet from 'helmet';
|
||||
|
||||
const schemaPath = resolvePackagePath(
|
||||
'@backstage/plugin-graphql-backend',
|
||||
@@ -28,6 +32,7 @@ const schemaPath = resolvePackagePath(
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
@@ -35,16 +40,39 @@ export async function createRouter(
|
||||
): Promise<express.Router> {
|
||||
const typeDefs = await fs.promises.readFile(schemaPath, 'utf-8');
|
||||
|
||||
const server = new ApolloServer({ typeDefs, logger: options.logger });
|
||||
const router = Router();
|
||||
const catalogModule = await createCatalogModule(options);
|
||||
|
||||
const apolloMiddleware = server.getMiddleware({ path: '/' });
|
||||
router.use(apolloMiddleware);
|
||||
const { schema } = new GraphQLModule({
|
||||
imports: [catalogModule],
|
||||
typeDefs,
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
logger: options.logger,
|
||||
introspection: true,
|
||||
playground: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
response.send({ status: 'ok' });
|
||||
});
|
||||
|
||||
const apolloMiddleware = server.getMiddleware({ path: '/' });
|
||||
|
||||
if (process.env.NODE_ENV === 'development')
|
||||
router.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'", "'unsafe-inline'", 'http://*'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
router.use(apolloMiddleware);
|
||||
|
||||
router.use(errorHandler());
|
||||
|
||||
return router;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 { createServiceBuilder } from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'graphql-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
});
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
.addRouter('/graphql', router);
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
@@ -13,6 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
require('whatwg-fetch');
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-identity-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,7 +20,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
@@ -33,7 +33,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-jenkins",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -36,8 +36,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-lighthouse",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,9 +21,9 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -34,9 +34,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-newrelic",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,8 +21,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-proxy-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -19,8 +19,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/http-proxy-middleware": "^0.19.3",
|
||||
"express": "^4.17.1",
|
||||
@@ -35,7 +35,7 @@
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/uuid": "^8.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-register-component",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -36,8 +36,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-rollbar-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,8 +20,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"axios": "^0.20.0",
|
||||
"camelcase-keys": "^6.2.2",
|
||||
@@ -37,7 +37,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"supertest": "^4.0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-rollbar",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -37,9 +37,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,9 +20,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@gitbeaker/core": "^23.5.0",
|
||||
"@gitbeaker/node": "^23.5.0",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
@@ -46,7 +46,7 @@
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@octokit/types": "^5.4.1",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -41,9 +41,9 @@
|
||||
"swr": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-sentry-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,7 +20,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"axios": "^0.20.0",
|
||||
"compression": "^1.7.4",
|
||||
@@ -34,7 +34,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-sentry",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,9 +21,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -36,8 +36,8 @@
|
||||
"timeago.js": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-tech-radar",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,9 +21,9 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -35,8 +35,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-techdocs-backend",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,9 +20,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@types/dockerode": "^2.5.34",
|
||||
"@types/express": "^4.17.6",
|
||||
"command-exists-promise": "^2.0.2",
|
||||
@@ -38,7 +38,7 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'techdocs-backend' });
|
||||
const config = ConfigReader.fromConfigs([]);
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const preparers = new Preparers();
|
||||
@@ -45,7 +46,7 @@ export async function startStandaloneServer(
|
||||
preparers.register('dir', directoryPreparer);
|
||||
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator(logger);
|
||||
const techdocsGenerator = new TechdocsGenerator(logger, config);
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
|
||||
const publisher = new LocalPublish(logger);
|
||||
@@ -59,7 +60,7 @@ export async function startStandaloneServer(
|
||||
logger,
|
||||
publisher,
|
||||
dockerClient,
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
config,
|
||||
});
|
||||
const service = createServiceBuilder(module)
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { Generators, TechdocsGenerator } from './';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -38,7 +39,10 @@ describe('generators', () => {
|
||||
|
||||
it('should return correct registered generator', async () => {
|
||||
const generators = new Generators();
|
||||
const techdocs = new TechdocsGenerator(logger);
|
||||
const techdocs = new TechdocsGenerator(
|
||||
logger,
|
||||
ConfigReader.fromConfigs([]),
|
||||
);
|
||||
|
||||
generators.register('techdocs', techdocs);
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { Logger } from 'winston';
|
||||
import { PassThrough } from 'stream';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
import {
|
||||
GeneratorBase,
|
||||
@@ -26,18 +28,39 @@ import {
|
||||
} from './types';
|
||||
import { runDockerContainer, runCommand } from './helpers';
|
||||
|
||||
const commandExists = require('command-exists-promise');
|
||||
type TechdocsGeneratorOptions = {
|
||||
// This option enables users to configure if they want to use TechDocs container
|
||||
// or generate without the container.
|
||||
// This is used to avoid running into Docker in Docker environment.
|
||||
runGeneratorIn: string;
|
||||
};
|
||||
|
||||
const createStream = (): [string[], PassThrough] => {
|
||||
const log = [] as Array<string>;
|
||||
|
||||
const stream = new PassThrough();
|
||||
stream.on('data', chunk => {
|
||||
const textValue = chunk.toString().trim();
|
||||
if (textValue?.length > 1) log.push(textValue);
|
||||
});
|
||||
|
||||
return [log, stream];
|
||||
};
|
||||
|
||||
export class TechdocsGenerator implements GeneratorBase {
|
||||
private readonly logger: Logger;
|
||||
private readonly options: TechdocsGeneratorOptions;
|
||||
|
||||
constructor(logger: Logger) {
|
||||
constructor(logger: Logger, config: Config) {
|
||||
this.logger = logger;
|
||||
this.options = {
|
||||
runGeneratorIn:
|
||||
config.getOptionalString('techdocs.generators.techdocs') ?? 'docker',
|
||||
};
|
||||
}
|
||||
|
||||
public async run({
|
||||
directory,
|
||||
logStream,
|
||||
dockerClient,
|
||||
}: GeneratorRunOptions): Promise<GeneratorRunResult> {
|
||||
const tmpdirPath = os.tmpdir();
|
||||
@@ -46,35 +69,46 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
const resultDir = fs.mkdtempSync(
|
||||
path.join(tmpdirResolvedPath, 'techdocs-tmp-'),
|
||||
);
|
||||
const [log, logStream] = createStream();
|
||||
|
||||
try {
|
||||
const mkdocsInstalled = await commandExists('mkdocs');
|
||||
if (mkdocsInstalled) {
|
||||
await runCommand({
|
||||
command: 'mkdocs',
|
||||
args: ['build', '-d', resultDir, '-v'],
|
||||
options: {
|
||||
cwd: directory,
|
||||
},
|
||||
logStream,
|
||||
});
|
||||
} else {
|
||||
await runDockerContainer({
|
||||
imageName: 'spotify/techdocs',
|
||||
args: ['build', '-d', '/result'],
|
||||
logStream,
|
||||
docsDir: directory,
|
||||
resultDir,
|
||||
dockerClient,
|
||||
});
|
||||
this.logger.info(
|
||||
`[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`,
|
||||
);
|
||||
switch (this.options.runGeneratorIn) {
|
||||
case 'local':
|
||||
await runCommand({
|
||||
command: 'mkdocs',
|
||||
args: ['build', '-d', resultDir, '-v'],
|
||||
options: {
|
||||
cwd: directory,
|
||||
},
|
||||
logStream,
|
||||
});
|
||||
this.logger.info(
|
||||
`[TechDocs]: Successfully generated docs from ${directory} into ${resultDir} using local mkdocs`,
|
||||
);
|
||||
break;
|
||||
case 'docker':
|
||||
await runDockerContainer({
|
||||
imageName: 'spotify/techdocs',
|
||||
args: ['build', '-d', '/result'],
|
||||
logStream,
|
||||
docsDir: directory,
|
||||
resultDir,
|
||||
dockerClient,
|
||||
});
|
||||
this.logger.info(
|
||||
`[TechDocs]: Successfully generated docs from ${directory} into ${resultDir} using techdocs-container`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid config value "${this.options.runGeneratorIn}" provided in 'techdocs.generators.techdocs'.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug(
|
||||
`[TechDocs]: Failed to generate docs from ${directory} into ${resultDir}`,
|
||||
);
|
||||
this.logger.debug(`[TechDocs]: Build failed with error: ${log}`);
|
||||
throw new Error(
|
||||
`Failed to generate docs from ${directory} into ${resultDir} with error ${error.message}`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-techdocs",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -22,12 +22,12 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/core-api": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/core-api": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -40,8 +40,8 @@
|
||||
"sanitize-html": "^1.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-welcome",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
@@ -21,8 +21,8 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.22",
|
||||
"@backstage/theme": "^0.1.1-alpha.22",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -32,8 +32,8 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.21",
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.22",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
Reference in New Issue
Block a user