Merge branch 'ebarriosjr/Creating-gcp-projects-plugin' of github.com:ebarriosjr/backstage into ebarriosjr/Creating-gcp-projects-plugin

This commit is contained in:
ebarrios
2020-07-14 07:40:21 +02:00
67 changed files with 1942 additions and 379 deletions
+6 -3
View File
@@ -4,6 +4,9 @@
# The last matching pattern takes precedence.
# https://help.github.com/articles/about-codeowners/
* @spotify/backstage-core
/plugins/techdocs @spotify/techdocs-core
/packages/techdocs-cli @spotify/techdocs-core
* @spotify/backstage-core
/docs/features/techdocs @spotify/techdocs-core
/plugins/techdocs @spotify/techdocs-core
/plugins/techdocs-backend @spotify/techdocs-core
/packages/techdocs-cli @spotify/techdocs-core
/packages/techdocs-container @spotify/techdocs-core
+3
View File
@@ -61,6 +61,9 @@ typings/
# Optional npm cache directory
.npm
# Node version directives
.nvmrc
# Optional eslint cache
.eslintcache
+7
View File
@@ -10,6 +10,13 @@ backend:
methods: [GET, POST, PUT, DELETE]
credentials: true
proxy:
'/circleci/api':
target: 'https://circleci.com/api/v1.1'
changeOrigin: true
pathRewrite:
'^/circleci/api/': '/'
organization:
name: Spotify
+1
View File
@@ -0,0 +1 @@
# FAQ
+42
View File
@@ -0,0 +1,42 @@
# TechDocs Documentation
## What is it?
Intro, backstory, etc.
## Structure
- [Getting Started]
- [Concepts]
- [Reading Documentation]
- [Writing Documentation]
- [Publishing Documentation]
- [Contributing]
- [Debugging]
- [FAQ]
## Tech Stack
| Stack | Location |
| ------------------------------------------- | -------------------------------------------------------- |
| Frontend | [`@backstage/plugin-techdocs`][techdocs/frontend] |
| Backend | [`@backstage/plugin-techdocs-backend`][techdocs/backend] |
| Docker Container (for generating doc sites) | [`packages/techdocs-container`][techdocs/container] |
| CLI (for local development) | [`packages/techdocs-cli`][techdocs/cli] |
[getting started]: getting-started.md
[concepts]: concepts.md
[reading documentation]: reading-documentation.md
[writing documentation]: writing-documentation.md
[publishing documentation]: publishing-documentation.md
[contributing]: contributing.md
[debugging]: debugging.md
[faq]: FAQ.md 'Frequently asked questions'
[techdocs/frontend]:
https://github.com/spotify/backstage/blob/master/plugins/techdocs
[techdocs/backend]:
https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend
[techdocs/container]:
https://github.com/spotify/backstage/blob/master/packages/techdocs-container
[techdocs/cli]:
https://github.com/spotify/backstage/blob/master/packages/techdocs-cli
+1
View File
@@ -0,0 +1 @@
# Concepts
+1
View File
@@ -0,0 +1 @@
# Contributing
+1
View File
@@ -0,0 +1 @@
# Debugging
@@ -0,0 +1 @@
# Getting Started
@@ -0,0 +1 @@
# Publishing Documentation
@@ -0,0 +1 @@
# Reading Documentation
@@ -0,0 +1 @@
# Writing Documentation
@@ -95,4 +95,13 @@ There are two things needed for a Backstage app to start making use of a plugin.
Luckily these two steps happen automatically when you create a plugin with the
Backstage CLI.
## Talking to the outside world
If your plugin needs to communicate with services outside the backstage
environment you will probably face challenges like CORS policies and/or
backend-side authorization. To smooth this process out you can use proxy -
either the one you already have (like nginx/haproxy/etc) or the proxy-backend
plugin that we provide for the backstage backend.
[Read more](../../plugins/proxy-backend/README.md)
[Back to Getting Started](README.md)
+10
View File
@@ -59,6 +59,11 @@ import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar';
import { GCPClient, GCPApiRef } from '@backstage/plugin-gcp-projects';
import {
GithubActionsClient,
githubActionsApiRef,
} from '@backstage/plugin-github-actions';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
@@ -77,6 +82,11 @@ export const apis = (config: ConfigApi) => {
builder.add(storageApiRef, WebStorage.create({ errorApi }));
builder.add(circleCIApiRef, new CircleCIApi());
builder.add(GCPApiRef, new GCPClient());
builder.add(
circleCIApiRef,
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
);
builder.add(githubActionsApiRef, new GithubActionsClient());
builder.add(featureFlagsApiRef, new FeatureFlags());
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
+2
View File
@@ -18,6 +18,7 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.13",
"@backstage/backend-common": "^0.1.1-alpha.13",
"@backstage/catalog-model": "^0.1.1-alpha.13",
"@backstage/config": "^0.1.1-alpha.13",
@@ -29,6 +30,7 @@
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.13",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.13",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.13",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
"knex": "^0.21.1",
+8 -11
View File
@@ -36,6 +36,7 @@ import identity from './plugins/identity';
import rollbar from './plugins/rollbar';
import scaffolder from './plugins/scaffolder';
import sentry from './plugins/sentry';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import { PluginEnvironment } from './types';
@@ -65,25 +66,21 @@ async function main() {
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar'));
const sentryEnv = useHotMemoize(module, () => createEnv('sentry'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter(
'/rollbar',
await rollbar(
getRootLogger().child({ type: 'plugin', plugin: 'rollbar' }),
),
)
.addRouter('/rollbar', await rollbar(rollbarEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter(
'/sentry',
await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })),
)
.addRouter('/sentry', await sentry(sentryEnv))
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv))
.addRouter('/techdocs', await techdocs(techdocsEnv));
.addRouter('/techdocs', await techdocs(techdocsEnv))
.addRouter('/proxy', await proxy(proxyEnv));
await service.start().catch(err => {
console.log(err);
@@ -13,26 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-ignore
import { createRouter } from '@backstage/plugin-proxy-backend';
import { PluginEnvironment } from '../types';
export enum BuildStatus {
Null,
Success,
Failure,
Pending,
Running,
export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config });
}
export type Build = {
commitId: string;
message: string;
branch: string;
status: BuildStatus;
uri: string;
};
export type BuildDetails = {
build: Build;
author: string;
logUrl: string;
overviewUrl: string;
};
+2 -2
View File
@@ -15,8 +15,8 @@
*/
import { createRouter } from '@backstage/plugin-rollbar-backend';
import { Logger } from 'winston';
import type { PluginEnvironment } from '../types';
export default async function createPlugin(logger: Logger) {
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({ logger });
}
+18 -3
View File
@@ -20,19 +20,34 @@ import {
FilePreparer,
GithubPreparer,
Preparers,
GithubPublisher,
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import { Octokit } from '@octokit/rest';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger }: PluginEnvironment) {
const templater = new CookieCutter();
const cookiecutterTemplater = new CookieCutter();
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
const filePreparer = new FilePreparer();
const githubPreparer = new GithubPreparer();
const preparers = new Preparers();
const dockerClient = new Docker();
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
return await createRouter({ preparers, templater, logger, dockerClient });
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
const dockerClient = new Docker();
return await createRouter({
preparers,
templaters,
publisher,
logger,
dockerClient,
});
}
+2 -2
View File
@@ -15,8 +15,8 @@
*/
import { createRouter } from '@backstage/plugin-sentry-backend';
import { Logger } from 'winston';
import type { PluginEnvironment } from '../types';
export default async function createPlugin(logger: Logger) {
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter(logger);
}
@@ -32,7 +32,8 @@ describe('TemplateEntityV1alpah1', () => {
name: 'test',
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
@@ -78,7 +79,7 @@ describe('TemplateEntityV1alpah1', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('acceptps any other type', async () => {
it('accepts any other type', async () => {
(entity as any).spec.type = 'hallo';
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
@@ -87,4 +88,9 @@ describe('TemplateEntityV1alpah1', () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects missing templater', async () => {
(entity as any).spec.templater = '';
await expect(policy.enforce(entity)).rejects.toThrow(/templater/);
});
});
@@ -26,6 +26,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
kind: typeof KIND;
spec: {
type: string;
templater: string;
path?: string;
schema: JSONSchema;
};
@@ -43,6 +44,7 @@ export class TemplateEntityV1alpha1Policy implements EntityPolicy {
type: yup.string().required().min(1),
path: yup.string(),
schema: yup.object().required(),
templater: yup.string().required(),
})
.required(),
});
+3 -3
View File
@@ -9,15 +9,15 @@ This is the Docker container that powers the creation of static documentation si
Using the TechDocs CLI, we can invoke the latest version of `techdocs-container` via Docker Hub:
```bash
npx @techdocs/cli serve:container
npx @techdocs/cli serve
```
## Local Development
```bash
docker build ./container -t techdocs-container
docker build . -t mkdocs:local-dev
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it techdocs-container serve -a 0.0.0.0:8000
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs:local-dev serve -a 0.0.0.0:8000
```
Then open up `http://localhost:8000` on your local machine.
@@ -26,6 +26,7 @@ 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 { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
import { LocationRefProcessor } from './processors/LocationEntityProcessor';
import * as result from './processors/results';
@@ -57,6 +58,7 @@ export class LocationReaders implements LocationReader {
return [
new FileReaderProcessor(),
new GithubReaderProcessor(),
new GithubApiReaderProcessor(),
new GitlabReaderProcessor(),
new YamlProcessor(),
new EntityPolicyProcessor(entityPolicy),
@@ -0,0 +1,89 @@
/*
* 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 { GithubApiReaderProcessor } from './GithubApiReaderProcessor';
describe('GithubApiReaderProcessor', () => {
it('should build raw api', () => {
const processor = new GithubApiReaderProcessor();
const tests = [
{
target: 'https://github.com/a/b/blob/master/path/to/c.yaml',
url: new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=master',
),
err: undefined,
},
{
target: 'https://api.com/a/b/blob/master/path/to/c.yaml',
url: null,
err:
'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong GitHub URL or Invalid file path',
},
{
target: 'com/a/b/blob/master/path/to/c.yaml',
url: null,
err:
'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
},
{
target:
'https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml',
url: new URL(
'https://api.github.com/repos/spotify/backstage/contents/packages/catalog-model/examples/playback-order-component.yaml?ref=master',
),
err: undefined,
},
];
for (const test of tests) {
if (test.err) {
expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err);
} else {
expect(processor.buildRawUrl(test.target)).toEqual(test.url);
}
}
});
it('should return request options', () => {
const tests = [
{
token: '0123456789',
expect: {
headers: {
Accept: 'application/vnd.github.v3.raw',
Authorization: 'token 0123456789',
},
},
},
{
token: '',
expect: {
headers: {
Accept: 'application/vnd.github.v3.raw',
},
},
},
];
for (const test of tests) {
process.env.GITHUB_PRIVATE_TOKEN = test.token;
const processor = new GithubApiReaderProcessor();
expect(processor.getRequestOptions()).toEqual(test.expect);
}
});
});
@@ -0,0 +1,121 @@
/*
* 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 fetch, { RequestInit, HeadersInit } from 'node-fetch';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
export class GithubApiReaderProcessor implements LocationProcessor {
private privateToken: string = process.env.GITHUB_PRIVATE_TOKEN || '';
getRequestOptions(): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (this.privateToken !== '') {
headers.Authorization = `token ${this.privateToken}`;
}
const requestOptions: RequestInit = {
headers,
};
return requestOptions;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: LocationProcessorEmit,
): Promise<boolean> {
if (location.type !== 'github/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString(), this.getRequestOptions());
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;
}
// Converts
// from: https://github.com/a/b/blob/master/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=master
buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
repoName,
blobKeyword,
ref,
...restOfPath
] = url.pathname.split('/');
if (
url.hostname !== 'github.com' ||
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
blobKeyword !== 'blob' ||
!restOfPath.join('/').match(/\.yaml$/)
) {
throw new Error('Wrong GitHub URL or Invalid file path');
}
// transform to api
url.pathname = [
empty,
'repos',
userOrOrg,
repoName,
'contents',
...restOfPath,
].join('/');
url.hostname = 'api.github.com';
url.protocol = 'https';
url.search = `ref=${ref}`;
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
-9
View File
@@ -10,15 +10,6 @@
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"proxy": {
"/circleci/api": {
"target": "https://circleci.com/api/v1.1",
"changeOrigin": true,
"pathRewrite": {
"^/circleci/api/": "/"
}
}
},
"scripts": {
"build": "backstage-cli plugin:build",
"lint": "backstage-cli lint",
+2
View File
@@ -21,7 +21,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.13",
"@backstage/core": "^0.1.1-alpha.13",
"@backstage/core-api": "^0.1.1-alpha.13",
"@backstage/theme": "^0.1.1-alpha.13",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
@@ -0,0 +1,36 @@
/*
* 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 { createApiRef } from '@backstage/core';
import { Build, BuildDetails } from './types';
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
id: 'plugin.githubactions.service',
description: 'Used by the Github Actions plugin to make requests',
});
export type GithubActionsApi = {
listBuilds: ({
owner,
repo,
token,
}: {
owner: string;
repo: string;
token: string;
}) => Promise<Build[]>;
getBuild: (buildUri: string, token: Promise<string>) => Promise<BuildDetails>;
};
@@ -0,0 +1,131 @@
/*
* 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 { GithubActionsApi } from './GithubActionsApi';
import { Build, BuildDetails, BuildStatus, WorkflowRun } from './types';
const statusToBuildStatus: { [status: string]: BuildStatus } = {
success: BuildStatus.Success,
failure: BuildStatus.Failure,
pending: BuildStatus.Pending,
running: BuildStatus.Running,
in_progress: BuildStatus.Running,
completed: BuildStatus.Success,
};
const conclusionToStatus = (conslusion: string): BuildStatus =>
statusToBuildStatus[conslusion] ?? BuildStatus.Null;
export class GithubActionsClient implements GithubActionsApi {
async listBuilds({
owner,
repo,
token,
}: {
owner: string;
repo: string;
token: string;
}): Promise<Build[]> {
const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs`;
const response = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${token}`,
}),
});
if (!response.ok) {
return [
{
commitId: 'Error',
message: 'Response status is not OK',
branch: 'Error',
status: BuildStatus.Failure,
uri: 'Error',
},
];
}
const data = await response.json();
const newData: WorkflowRun[] = data.workflow_runs;
const endData: Build[] = [];
newData.forEach((element, index) => {
const transData: Build = {
commitId: '',
message: '',
branch: '',
status: BuildStatus.Null,
uri: '',
};
transData.commitId = String(element.head_commit.id);
transData.branch = element.head_branch;
transData.status = conclusionToStatus(element.conclusion);
transData.message = element.head_commit.message;
transData.uri = element.url;
endData[index] = transData;
});
return endData;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getBuild(
buildUri: string,
token: Promise<string>,
): Promise<BuildDetails> {
const response = await fetch(buildUri, {
headers: new Headers({
Authorization: `Bearer ${await token}`,
}),
});
const buildBlank: Build = {
commitId: '',
message: '',
branch: '',
status: BuildStatus.Null,
uri: '',
};
const dataBlank: BuildDetails = {
build: buildBlank,
author: '',
logUrl: '',
overviewUrl: '',
};
if (!response.ok) {
return dataBlank;
}
const data = await response.json();
const newData: WorkflowRun = data;
dataBlank.author = newData.head_commit.author.name;
dataBlank.build.branch = newData.head_branch;
dataBlank.build.commitId = newData.head_commit.id;
dataBlank.build.message = newData.head_commit.message;
dataBlank.build.status = conclusionToStatus(newData.status);
dataBlank.build.uri = newData.url;
dataBlank.logUrl = newData.logs_url;
dataBlank.overviewUrl = newData.html_url;
return dataBlank;
}
}
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export { BuildsClient } from './BuildsClient';
export * from './GithubActionsApi';
export * from './GithubActionsClient';
export * from './types';
+224
View File
@@ -0,0 +1,224 @@
/*
* 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.
*/
export enum BuildStatus {
Null,
Success,
Failure,
Pending,
Running,
}
export type Build = {
commitId: string;
message: string;
branch: string;
status: BuildStatus;
uri: string;
};
export type BuildDetails = {
build: Build;
author: string;
logUrl: string;
overviewUrl: string;
};
export interface Author {
name: string;
email: string;
}
export interface Committer {
name: string;
email: string;
}
export interface HeadCommit {
id: string;
tree_id: string;
message: string;
timestamp: Date;
author: Author;
committer: Committer;
}
export interface Owner {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface Repository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner;
html_url: string;
description?: any;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
}
export interface Owner2 {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface HeadRepository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner2;
html_url: string;
description?: any;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
}
export interface WorkflowRun {
id: number;
node_id: string;
head_branch: string;
head_sha: string;
run_number: number;
event: string;
status: string;
conclusion: string;
workflow_id: number;
url: string;
html_url: string;
pull_requests: any[];
created_at: Date;
updated_at: Date;
jobs_url: string;
logs_url: string;
check_suite_url: string;
artifacts_url: string;
cancel_url: string;
rerun_url: string;
workflow_url: string;
head_commit: HeadCommit;
repository: Repository;
head_repository: HeadRepository;
}
@@ -1,44 +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 { Build, BuildDetails, BuildStatus } from './types';
export class BuildsClient {
static create(): BuildsClient {
return new BuildsClient();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async listBuilds(_entityUri: string): Promise<Build[]> {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getBuild(_buildUri: string): Promise<BuildDetails> {
return {
build: {
commitId: 'TODO',
branch: 'TODO',
uri: 'TODO',
status: BuildStatus.Running,
message: 'TODO',
},
author: 'TODO',
logUrl: 'TODO',
overviewUrl: 'TODO',
};
}
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
Button,
ButtonGroup,
@@ -30,10 +29,11 @@ import {
Typography,
} from '@material-ui/core';
import React from 'react';
import { useParams } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useAsync } from 'react-use';
import { BuildsClient } from '../../apis/builds';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
import { githubActionsApiRef } from '../../api';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -48,12 +48,18 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
const client = BuildsClient.create();
export const BuildDetailsPage = () => {
const api = useApi(githubActionsApiRef);
const githubApi = useApi(githubAuthApiRef);
const token = githubApi.getAccessToken('repo');
const classes = useStyles();
const { buildUri } = useParams();
const status = useAsync(() => client.getBuild(buildUri), [buildUri]);
const location = useLocation();
const status = useAsync(
() =>
api.getBuild(decodeURIComponent(location.search.split('uri=')[1]), token),
[location.search],
);
if (status.loading) {
return <LinearProgress />;
@@ -70,7 +76,7 @@ export const BuildDetailsPage = () => {
return (
<div className={classes.root}>
<Typography className={classes.title} variant="h3">
<Link to="/builds">
<Link to="/github-actions">
<Typography component="span" variant="h3" color="primary">
&lt;
</Typography>
@@ -124,12 +130,12 @@ export const BuildDetailsPage = () => {
>
{details?.overviewUrl && (
<Button>
<Link to={details.overviewUrl}>GitHub</Link>
<a href={details.overviewUrl}>GitHub</a>
</Button>
)}
{details?.logUrl && (
<Button>
<Link to={details.logUrl}>Logs</Link>
<a href={details.logUrl}>Logs</a>
</Button>
)}
</ButtonGroup>
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
LinearProgress,
makeStyles,
@@ -27,10 +26,9 @@ import {
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { BuildsClient } from '../../apis/builds';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
const client = BuildsClient.create();
import { githubActionsApiRef } from '../../api';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -41,62 +39,69 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
export const BuildInfoCard = () => {
const classes = useStyles();
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
const BuildInfoCardContent = () => {
const api = useApi(githubActionsApiRef);
const githubApi = useApi(githubAuthApiRef);
let content: JSX.Element;
const status = useAsync(async () => {
const token = await githubApi.getAccessToken('repo');
return api.listBuilds({ owner: 'spotify', repo: 'backstage', token });
});
if (status.loading) {
content = <LinearProgress />;
return <LinearProgress />;
} else if (status.error) {
content = (
return (
<Typography variant="h2" color="error">
Failed to load builds, {status.error.message}
</Typography>
);
} else {
const [build] =
status.value?.filter(({ branch }) => branch === 'master') ?? [];
content = (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
<Typography color="primary">{build?.message}</Typography>
</Link>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{build?.commitId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>
<BuildStatusIndicator status={build?.status} />
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
const [build] =
status.value?.filter(({ branch }) => branch === 'master') ?? [];
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
<Typography color="primary">{build?.message}</Typography>
</Link>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{build?.commitId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>
<BuildStatusIndicator status={build?.status} />
</TableCell>
</TableRow>
</TableBody>
</Table>
);
};
export const BuildInfoCard = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h2" className={classes.title}>
Master Build
</Typography>
{content}
<BuildInfoCardContent />
</div>
);
};
@@ -14,115 +14,43 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
LinearProgress,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Theme,
Tooltip,
Typography,
} from '@material-ui/core';
Header,
HeaderLabel,
pageTheme,
Page,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core';
import { Grid } from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { BuildsClient } from '../../apis/builds';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
const client = BuildsClient.create();
const LongText = ({ text, max }: { text: string; max: number }) => {
if (text.length < max) {
return <span>{text}</span>;
}
return (
<Tooltip title={text}>
<span>{text.slice(0, max)}...</span>
</Tooltip>
);
};
const useStyles = makeStyles<Theme>(theme => ({
root: {
padding: theme.spacing(2),
},
title: {
padding: theme.spacing(1, 0, 2, 0),
},
}));
const PageContents = () => {
const { loading, error, value } = useAsync(() =>
client.listBuilds('entity:spotify:backstage'),
);
if (loading) {
return <LinearProgress />;
}
if (error) {
return (
<Typography variant="h2" color="error">
Failed to load builds, {error.message}{' '}
</Typography>
);
}
return (
<TableContainer component={Paper}>
<Table aria-label="CI/CD builds table">
<TableHead>
<TableRow>
<TableCell>Status</TableCell>
<TableCell>Branch</TableCell>
<TableCell>Message</TableCell>
<TableCell>Commit</TableCell>
</TableRow>
</TableHead>
<TableBody>
{value!.map(build => (
<TableRow key={build.uri}>
<TableCell>
<BuildStatusIndicator status={build.status} />
</TableCell>
<TableCell>
<Typography>
<LongText text={build.branch} max={30} />
</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build.uri)}`}>
<Typography color="primary">
<LongText text={build.message} max={60} />
</Typography>
</Link>
</TableCell>
<TableCell>
<Tooltip title={build.commitId}>
<Typography noWrap>{build.commitId.slice(0, 10)}</Typography>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
import { BuildListTable } from '../BuildListTable';
export const BuildListPage = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h3" className={classes.title}>
CI/CD Builds
</Typography>
<PageContents />
</div>
<Page theme={pageTheme.tool}>
<Header
title="GitHub Actions"
subtitle="See recent builds and their status"
>
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="All builds">
<SupportButton>
This plugin allows you to view and interact with your builds within
the GitHub Actions environment.
</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<BuildListTable />
</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -0,0 +1,176 @@
/*
* 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 React, { FC } from 'react';
import { Link, Typography, Box, IconButton } from '@material-ui/core';
import RetryIcon from '@material-ui/icons/Replay';
import GitHubIcon from '@material-ui/icons/GitHub';
import { Link as RouterLink } from 'react-router-dom';
import {
StatusError,
StatusWarning,
StatusOK,
StatusPending,
StatusRunning,
Table,
TableColumn,
} from '@backstage/core';
import { useBuilds } from './useBuilds';
export type Build = {
id: string;
buildName: string;
buildUrl?: string;
source: {
branchName: string;
commit: {
hash: string;
url: string;
};
};
status: string;
onRestartClick: () => void;
};
// retried, canceled, infrastructure_fail, timedout, not_run, running, failed, queued, scheduled, not_running, no_tests, fixed, success
const getStatusComponent = (status: string | undefined = '') => {
switch (status.toLowerCase()) {
case 'queued':
case 'scheduled':
return <StatusPending />;
case 'running':
return <StatusRunning />;
case 'failed':
return <StatusError />;
case 'success':
return <StatusOK />;
case 'canceled':
default:
return <StatusWarning />;
}
};
const generatedColumns: TableColumn[] = [
{
title: 'ID',
field: 'id',
type: 'numeric',
width: '150px',
},
{
title: 'Build',
field: 'buildName',
highlight: true,
render: (row: Partial<Build>) => (
<Link component={RouterLink} to={`/build/${row.id}`}>
{row.buildName}
</Link>
),
},
{
title: 'Source',
render: (row: Partial<Build>) => (
<>
<p>{row.source?.branchName}</p>
<p>{row.source?.commit.hash}</p>
</>
),
},
{
title: 'Status',
render: (row: Partial<Build>) => (
<Box display="flex" alignItems="center">
{getStatusComponent(row.status)}
<Box mr={1} />
<Typography variant="button">{row.status}</Typography>
</Box>
),
},
{
title: 'Actions',
render: (row: Partial<Build>) => (
<IconButton onClick={row.onRestartClick}>
<RetryIcon />
</IconButton>
),
width: '10%',
},
];
type Props = {
loading: boolean;
retry: () => void;
builds?: Build[];
projectName: string;
page: number;
onChangePage: (page: number) => void;
total: number;
pageSize: number;
onChangePageSize: (pageSize: number) => void;
};
const BuildListTableView: FC<Props> = ({
projectName,
loading,
pageSize,
page,
retry,
builds,
onChangePage,
onChangePageSize,
total,
}) => {
return (
<Table
isLoading={loading}
options={{ paging: true, pageSize, padding: 'dense' }}
totalCount={total}
page={page}
actions={[
{
icon: () => <RetryIcon />,
tooltip: 'Refresh Data',
isFreeAction: true,
onClick: () => retry(),
},
]}
data={builds ?? []}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangePageSize}
title={
<Box display="flex" alignItems="center">
<GitHubIcon />
<Box mr={1} />
<Typography variant="h6">{projectName}</Typography>
</Box>
}
columns={generatedColumns}
/>
);
};
const noop = () => {};
export const BuildListTable = () => {
const [tableProps] = useBuilds();
return (
<BuildListTableView
{...tableProps}
retry={noop}
onChangePageSize={noop}
onChangePage={noop}
/>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { BuildListTable } from './BuildListTable';
export type { Build } from './BuildListTable';
@@ -0,0 +1,330 @@
/*
* 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 { useCallback, useState } from 'react';
import { useAsyncRetry } from 'react-use';
import { Build } from './BuildListTable';
// TODO(shmidt-i): use real APIs
const useEntityGHSettings = () => ({ repo: 'test', owner: 'shmidt-i-test' });
const buildsMock = {
total_count: 1,
workflow_runs: [
{
id: 30433642,
node_id: 'MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ==',
head_branch: 'master',
head_sha: 'acb5820ced9479c074f688cc328bf03f341a511d',
run_number: 562,
event: 'push',
status: 'queued',
conclusion: null,
workflow_id: 159038,
url:
'https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642',
html_url: 'https://github.com/octo-org/octo-repo/actions/runs/30433642',
pull_requests: [],
created_at: '2020-01-22T19:33:08Z',
updated_at: '2020-01-22T19:33:08Z',
jobs_url:
'https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/jobs',
logs_url:
'https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/logs',
check_suite_url:
'https://api.github.com/repos/octo-org/octo-repo/check-suites/414944374',
artifacts_url:
'https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/artifacts',
cancel_url:
'https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/cancel',
rerun_url:
'https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/rerun',
workflow_url:
'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/159038',
head_commit: {
id: 'acb5820ced9479c074f688cc328bf03f341a511d',
tree_id: 'd23f6eedb1e1b9610bbc754ddb5197bfe7271223',
message: 'Create linter.yml',
timestamp: '2020-01-22T19:33:05Z',
author: {
name: 'Octo Cat',
email: 'octocat@github.com',
},
committer: {
name: 'GitHub',
email: 'noreply@github.com',
},
},
repository: {
id: 1296269,
node_id: 'MDEwOlJlcG9zaXRvcnkxMjk2MjY5',
name: 'Hello-World',
full_name: 'octocat/Hello-World',
owner: {
login: 'octocat',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url:
'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url:
'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url:
'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
private: false,
html_url: 'https://github.com/octocat/Hello-World',
description: 'This your first repo!',
fork: false,
url: 'https://api.github.com/repos/octocat/Hello-World',
archive_url:
'http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}',
assignees_url:
'http://api.github.com/repos/octocat/Hello-World/assignees{/user}',
blobs_url:
'http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}',
branches_url:
'http://api.github.com/repos/octocat/Hello-World/branches{/branch}',
collaborators_url:
'http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}',
comments_url:
'http://api.github.com/repos/octocat/Hello-World/comments{/number}',
commits_url:
'http://api.github.com/repos/octocat/Hello-World/commits{/sha}',
compare_url:
'http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}',
contents_url:
'http://api.github.com/repos/octocat/Hello-World/contents/{+path}',
contributors_url:
'http://api.github.com/repos/octocat/Hello-World/contributors',
deployments_url:
'http://api.github.com/repos/octocat/Hello-World/deployments',
downloads_url:
'http://api.github.com/repos/octocat/Hello-World/downloads',
events_url: 'http://api.github.com/repos/octocat/Hello-World/events',
forks_url: 'http://api.github.com/repos/octocat/Hello-World/forks',
git_commits_url:
'http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}',
git_refs_url:
'http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}',
git_tags_url:
'http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}',
git_url: 'git:github.com/octocat/Hello-World.git',
issue_comment_url:
'http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}',
issue_events_url:
'http://api.github.com/repos/octocat/Hello-World/issues/events{/number}',
issues_url:
'http://api.github.com/repos/octocat/Hello-World/issues{/number}',
keys_url:
'http://api.github.com/repos/octocat/Hello-World/keys{/key_id}',
labels_url:
'http://api.github.com/repos/octocat/Hello-World/labels{/name}',
languages_url:
'http://api.github.com/repos/octocat/Hello-World/languages',
merges_url: 'http://api.github.com/repos/octocat/Hello-World/merges',
milestones_url:
'http://api.github.com/repos/octocat/Hello-World/milestones{/number}',
notifications_url:
'http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}',
pulls_url:
'http://api.github.com/repos/octocat/Hello-World/pulls{/number}',
releases_url:
'http://api.github.com/repos/octocat/Hello-World/releases{/id}',
ssh_url: 'git@github.com:octocat/Hello-World.git',
stargazers_url:
'http://api.github.com/repos/octocat/Hello-World/stargazers',
statuses_url:
'http://api.github.com/repos/octocat/Hello-World/statuses/{sha}',
subscribers_url:
'http://api.github.com/repos/octocat/Hello-World/subscribers',
subscription_url:
'http://api.github.com/repos/octocat/Hello-World/subscription',
tags_url: 'http://api.github.com/repos/octocat/Hello-World/tags',
teams_url: 'http://api.github.com/repos/octocat/Hello-World/teams',
trees_url:
'http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}',
},
head_repository: {
id: 217723378,
node_id: 'MDEwOlJlcG9zaXRvcnkyMTc3MjMzNzg=',
name: 'octo-repo',
full_name: 'octo-org/octo-repo',
private: true,
owner: {
login: 'octocat',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url:
'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url:
'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url:
'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
html_url: 'https://github.com/octo-org/octo-repo',
description: null,
fork: false,
url: 'https://api.github.com/repos/octo-org/octo-repo',
forks_url: 'https://api.github.com/repos/octo-org/octo-repo/forks',
keys_url:
'https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}',
collaborators_url:
'https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}',
teams_url: 'https://api.github.com/repos/octo-org/octo-repo/teams',
hooks_url: 'https://api.github.com/repos/octo-org/octo-repo/hooks',
issue_events_url:
'https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}',
events_url: 'https://api.github.com/repos/octo-org/octo-repo/events',
assignees_url:
'https://api.github.com/repos/octo-org/octo-repo/assignees{/user}',
branches_url:
'https://api.github.com/repos/octo-org/octo-repo/branches{/branch}',
tags_url: 'https://api.github.com/repos/octo-org/octo-repo/tags',
blobs_url:
'https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}',
git_tags_url:
'https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}',
git_refs_url:
'https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}',
trees_url:
'https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}',
statuses_url:
'https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}',
languages_url:
'https://api.github.com/repos/octo-org/octo-repo/languages',
stargazers_url:
'https://api.github.com/repos/octo-org/octo-repo/stargazers',
contributors_url:
'https://api.github.com/repos/octo-org/octo-repo/contributors',
subscribers_url:
'https://api.github.com/repos/octo-org/octo-repo/subscribers',
subscription_url:
'https://api.github.com/repos/octo-org/octo-repo/subscription',
commits_url:
'https://api.github.com/repos/octo-org/octo-repo/commits{/sha}',
git_commits_url:
'https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}',
comments_url:
'https://api.github.com/repos/octo-org/octo-repo/comments{/number}',
issue_comment_url:
'https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}',
contents_url:
'https://api.github.com/repos/octo-org/octo-repo/contents/{+path}',
compare_url:
'https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}',
merges_url: 'https://api.github.com/repos/octo-org/octo-repo/merges',
archive_url:
'https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}',
downloads_url:
'https://api.github.com/repos/octo-org/octo-repo/downloads',
issues_url:
'https://api.github.com/repos/octo-org/octo-repo/issues{/number}',
pulls_url:
'https://api.github.com/repos/octo-org/octo-repo/pulls{/number}',
milestones_url:
'https://api.github.com/repos/octo-org/octo-repo/milestones{/number}',
notifications_url:
'https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}',
labels_url:
'https://api.github.com/repos/octo-org/octo-repo/labels{/name}',
releases_url:
'https://api.github.com/repos/octo-org/octo-repo/releases{/id}',
deployments_url:
'https://api.github.com/repos/octo-org/octo-repo/deployments',
},
},
],
};
export function useBuilds() {
const { repo, owner } = useEntityGHSettings();
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(5);
const getBuilds = useCallback(async () => buildsMock, []);
const restartBuild = async () => {};
const { loading, value: builds, retry } = useAsyncRetry(
() =>
getBuilds().then((allBuilds): Build[] => {
setTotal(allBuilds.total_count);
// Transformation here
return allBuilds.workflow_runs.map(run => ({
buildName: run.head_commit.message,
id: `${run.id}`,
onRestartClick: () => {},
source: {
branchName: run.head_branch,
commit: {
hash: run.head_commit.id,
url: run.head_repository.branches_url.replace(
'{/branch}',
run.head_branch,
),
},
},
status: run.status,
buildUrl: run.url,
}));
}),
[page, pageSize, getBuilds],
);
const projectName = `${owner}/${repo}`;
return [
{
page,
pageSize,
loading,
builds,
projectName,
total,
},
{
getBuilds,
setPage,
setPageSize,
restartBuild,
retry,
},
] as const;
}
@@ -21,7 +21,7 @@ import SuccessIcon from '@material-ui/icons/CheckCircle';
import FailureIcon from '@material-ui/icons/Error';
import UnknownIcon from '@material-ui/icons/Help';
import React from 'react';
import { BuildStatus } from '../../apis/builds';
import { BuildStatus } from '../../api/types';
type Props = {
status?: BuildStatus;
+1
View File
@@ -15,3 +15,4 @@
*/
export { plugin } from './plugin';
export * from './api';
+1 -1
View File
@@ -24,7 +24,7 @@ export const rootRouteRef = createRouteRef({
title: 'GitHub Actions',
});
export const buildRouteRef = createRouteRef({
path: '/github-actions/builds/:buildUri',
path: '/github-actions/builds',
title: 'GitHub Actions Build',
});
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+37
View File
@@ -0,0 +1,37 @@
# Proxy backend plugin
This is the backend plugin that enables proxy definitions to be declared in and read from app-config.yaml.
Relies on the `http-proxy-middleware` package.
## 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.
To run it within the backend do:
1. Register the router in `packages/backend/src/index.ts`:
```
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const service = createServiceBuilder(module)
.loadConfig(configReader)
/** several different routers */
.addRouter('/', await proxy(proxyEnv));
```
2. Start the backend
```bash
yarn workspace example-backend start
```
This will launch the full example backend.
## Links
- (http-proxy-middleware)[https://www.npmjs.com/package/http-proxy-middleware]
- (The Backstage homepage)[https://backstage.io]
+49
View File
@@ -0,0 +1,49 @@
{
"name": "@backstage/plugin-proxy-backend",
"version": "0.1.1-alpha.13",
"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",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.13",
"@backstage/config": "^0.1.1-alpha.13",
"@backstage/config-loader": "^0.1.1-alpha.13",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"http-proxy-middleware": "^0.19.1",
"morgan": "^1.10.0",
"node-fetch": "^2.6.0",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"yaml": "^1.9.2",
"yn": "^4.0.0",
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.13",
"@types/node-fetch": "^2.5.7",
"@types/supertest": "^2.0.8",
"@types/uuid": "^8.0.0",
"@types/yup": "^0.28.2",
"jest-fetch-mock": "^3.0.3",
"supertest": "^4.0.2"
},
"files": [
"dist"
]
}
+17
View File
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './service/router';
+33
View File
@@ -0,0 +1,33 @@
/*
* 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 { 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);
});
@@ -0,0 +1,32 @@
/*
* 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 { createRouter } from './router';
import winston from 'winston';
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
describe('createRouter', () => {
it('works', async () => {
const logger = winston.createLogger();
const config = ConfigReader.fromConfigs(await loadConfig());
const router = await createRouter({
config,
logger,
});
expect(router).toBeDefined();
});
});
@@ -0,0 +1,38 @@
/*
* 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 { Config } from '@backstage/config';
import express from 'express';
import Router from 'express-promise-router';
import createProxyMiddleware from 'http-proxy-middleware';
import { Logger } from 'winston';
export interface RouterOptions {
logger: Logger;
config: Config;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const router = Router();
const proxyConfig = options.config.get('proxy') ?? {};
Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => {
router.use(createProxyMiddleware(route, proxyRouteConfig));
});
return router;
}
@@ -0,0 +1,54 @@
/*
* 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';
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'proxy-backend' });
logger.debug('Creating application...');
const config = ConfigReader.fromConfigs(await loadConfig());
const router = await createRouter({
config,
logger,
});
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/proxy', router);
logger.debug('Starting application server...');
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
require('jest-fetch-mock').enableMocks();
export {};
+1 -1
View File
@@ -4,7 +4,7 @@ Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API.
## Setup
A `ROLLBAR_TOKEN` environment variable must be set to a read access account token.
A `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read access account token.
## Links
+37 -72
View File
@@ -30,7 +30,7 @@ export async function createRouter(
): Promise<express.Router> {
const router = Router();
const logger = options.logger.child({ plugin: 'rollbar' });
const accessToken = !options.rollbarApi ? getRollbarToken(logger) : '';
const accessToken = !options.rollbarApi ? getRollbarAccountToken(logger) : '';
if (options.rollbarApi || accessToken) {
const rollbarApi =
@@ -38,63 +38,43 @@ export async function createRouter(
router.use(express.json());
const runAsync = createRunAsyncWrapper(logger);
router.get('/projects', async (_req, res) => {
const projects = await rollbarApi.getAllProjects();
res.status(200).header('').send(projects);
});
router.get(
'/projects',
runAsync(async (_req, res) => {
const projects = await rollbarApi.getAllProjects();
res.status(200).header('').send(projects);
}),
);
router.get('/projects/:id', async (req, res) => {
const { id } = req.params;
const projects = await rollbarApi.getProject(id);
res.status(200).send(projects);
});
router.get(
'/projects/:id',
runAsync(async (req, res) => {
const { id } = req.params;
const projects = await rollbarApi.getProject(id);
res.status(200).send(projects);
}),
);
router.get('/projects/:id/items', async (req, res) => {
const { id } = req.params;
const projects = await rollbarApi.getProjectItems(id);
res.status(200).send(projects);
});
router.get(
'/projects/:id/items',
runAsync(async (req, res) => {
const { id } = req.params;
const projects = await rollbarApi.getProjectItems(id);
res.status(200).send(projects);
}),
);
router.get('/projects/:id/top_active_items', async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getTopActiveItems(id, query as any);
res.status(200).send(items);
});
router.get(
'/projects/:id/top_active_items',
runAsync(async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getTopActiveItems(id, query as any);
res.status(200).send(items);
}),
);
router.get('/projects/:id/occurance_counts', async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getOccuranceCounts(id, query as any);
res.status(200).send(items);
});
router.get(
'/projects/:id/occurance_counts',
runAsync(async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getOccuranceCounts(id, query as any);
res.status(200).send(items);
}),
);
router.get(
'/projects/:id/activated_item_counts',
runAsync(async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getActivatedCounts(id, query as any);
res.status(200).send(items);
}),
);
router.get('/projects/:id/activated_item_counts', async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getActivatedCounts(id, query as any);
res.status(200).send(items);
});
}
router.use(errorHandler());
@@ -102,32 +82,17 @@ export async function createRouter(
return router;
}
function createRunAsyncWrapper(logger: Logger) {
return function runAsyncWrapper(callback: express.RequestHandler) {
return function runAsync(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
return Promise.resolve(callback(req, res, next)).catch(error => {
logger.error(error);
next(error);
});
};
};
}
function getRollbarToken(logger: Logger) {
const token = process.env.ROLLBAR_TOKEN || '';
function getRollbarAccountToken(logger: Logger) {
const token = process.env.ROLLBAR_ACCOUNT_TOKEN || '';
if (!token) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Rollbar token must be provided in ROLLBAR_TOKEN environment variable to start the API.',
'Rollbar token must be provided in ROLLBAR_ACCOUNT_TOKEN environment variable to start the API.',
);
}
logger.warn(
'Failed to initialize rollbar backend, set ROLLBAR_TOKEN environment variable to start the API.',
'Failed to initialize rollbar backend, set ROLLBAR_ACCOUNT_TOKEN environment variable to start the API.',
);
}
@@ -8,7 +8,7 @@ metadata:
- Recommended
- React
spec:
processor: cookiecutter
templater: cookiecutter
type: website
path: '.'
schema:
@@ -8,7 +8,7 @@ metadata:
- Recommended
- Java
spec:
processor: cookiecutter
templater: cookiecutter
type: service
path: '.'
schema:
@@ -37,7 +37,8 @@ describe('JobProcessor', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -47,7 +47,8 @@ describe('GitHubPreparer', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -39,7 +39,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -86,7 +87,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -131,7 +133,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -177,7 +180,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -221,7 +225,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -24,7 +24,7 @@ describe('Preparers', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/Users/blam/dev/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
'file:/Users/bingo/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
},
name: 'react-ssr-template',
title: 'React SSR Template',
@@ -35,8 +35,9 @@ describe('Preparers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
templater: 'cookiecutter',
path: '.',
type: 'website',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
@@ -88,7 +89,8 @@ describe('Preparers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: '.',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './github';
export * from './types';
@@ -16,6 +16,8 @@
import { Writable, PassThrough } from 'stream';
import Docker from 'dockerode';
import fs from 'fs';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { InputError } from '@backstage/backend-common';
export type RunDockerContainerOptions = {
imageName: string;
@@ -26,6 +28,20 @@ export type RunDockerContainerOptions = {
dockerClient: Docker;
};
/**
* Gets the templater key to use for templating from the entity
* @param entity Template entity
*/
export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => {
const { templater } = entity.spec;
if (!templater) {
throw new InputError('Template does not have a required templating key');
}
return templater;
};
/**
*
* @param options the options object
@@ -16,3 +16,4 @@
export * from './cookiecutter';
export * from './types';
export * from './helpers';
export * from './templaters';
@@ -0,0 +1,125 @@
/*
* 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 { Templaters } from '.';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { CookieCutter } from './cookiecutter';
describe('Templaters', () => {
const mockTemplate: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/Users/bingo/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
},
name: 'react-ssr-template',
title: 'React SSR Template',
description:
'Next.js application skeleton for creating isomorphic web applications.',
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
generation: 1,
},
spec: {
templater: 'cookiecutter',
path: '.',
type: 'website',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
properties: {
owner: {
type: 'string',
title: 'Owner',
description: 'Who is going to own this component',
},
storePath: {
type: 'string',
title: 'Store path',
description: 'GitHub store path in org/repo format',
},
},
},
},
};
it('should throw an error when the templater is not registered', () => {
const templaters = new Templaters();
expect(() => templaters.get(mockTemplate)).toThrow(
expect.objectContaining({
message: 'No templater registered for template: "cookiecutter"',
}),
);
});
it('should return the correct templater when the templater matches', () => {
const templaters = new Templaters();
const templater = new CookieCutter();
templaters.register('cookiecutter', templater);
expect(templaters.get(mockTemplate)).toBe(templater);
});
it('should throw an error if the templater does not exist in the entity', () => {
const brokenTemplate: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {},
name: 'react-ssr-template',
title: 'React SSR Template',
description:
'Next.js application skeleton for creating isomorphic web applications.',
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
generation: 1,
},
spec: {
type: 'website',
path: '.',
templater: '',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
properties: {
owner: {
type: 'string',
title: 'Owner',
description: 'Who is going to own this component',
},
storePath: {
type: 'string',
title: 'Store path',
description: 'GitHub store path in org/repo format',
},
},
},
},
};
const templaters = new Templaters();
expect(() => templaters.get(brokenTemplate)).toThrow(
expect.objectContaining({
name: 'InputError',
message: expect.stringContaining(
'Template does not have a required templating key',
),
}),
);
});
});
@@ -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 {
TemplaterBase,
SupportedTemplatingKey,
TemplaterBuilder,
} from './types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { getTemplaterKey } from './helpers';
export class Templaters implements TemplaterBuilder {
private preparerMap = new Map<SupportedTemplatingKey, TemplaterBase>();
register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase) {
this.preparerMap.set(templaterKey, templater);
}
get(template: TemplateEntityV1alpha1): TemplaterBase {
const templaterKey = getTemplaterKey(template);
const preparer = this.preparerMap.get(templaterKey);
if (!preparer) {
throw new Error(
`No templater registered for template: "${templaterKey}"`,
);
}
return preparer;
}
}
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Writable } from 'stream';
import Docker from 'dockerode';
import { JsonValue } from '@backstage/config';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
/**
* Currently the required template values. The owner
@@ -55,3 +55,16 @@ export type TemplaterBase = {
export type TemplaterConfig = {
templater?: TemplaterBase;
};
/**
* List of supported templating options
*/
export type SupportedTemplatingKey = 'cookiecutter' | string;
/**
* The templater builder holds the templaters ready for run time
*/
export type TemplaterBuilder = {
register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void;
get(template: TemplateEntityV1alpha1): TemplaterBase;
};
@@ -16,23 +16,24 @@
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { Octokit } from '@octokit/rest';
import Docker from 'dockerode';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import {
GithubPublisher,
JobProcessor,
PreparerBuilder,
RequiredTemplateValues,
StageContext,
TemplaterBase,
TemplaterBuilder,
Publisher,
} from '../scaffolder';
export interface RouterOptions {
preparers: PreparerBuilder;
templater: TemplaterBase;
templaters: TemplaterBuilder;
publisher: Publisher;
logger: Logger;
dockerClient: Docker;
}
@@ -42,9 +43,14 @@ export async function createRouter(
): Promise<express.Router> {
const router = Router();
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const { preparers, templater, logger: parentLogger, dockerClient } = options;
const githubPulisher = new GithubPublisher({ client: githubClient });
const {
preparers,
templaters,
publisher,
logger: parentLogger,
dockerClient,
} = options;
const logger = parentLogger.child({ plugin: 'scaffolder' });
const jobProcessor = new JobProcessor();
@@ -106,6 +112,7 @@ export async function createRouter(
{
name: 'Run the templater',
handler: async (ctx: StageContext<{ skeletonDir: string }>) => {
const templater = templaters.get(ctx.entity);
const { resultDir } = await templater.run({
directory: ctx.skeletonDir,
dockerClient,
@@ -120,7 +127,8 @@ export async function createRouter(
name: 'Publish template',
handler: async (ctx: StageContext<{ resultDir: string }>) => {
ctx.logger.info('Should not store the template');
const { remoteUrl } = await githubPulisher.publish({
const { remoteUrl } = await publisher.publish({
entity: ctx.entity,
values: ctx.values,
directory: ctx.resultDir,
});
+14 -23
View File
@@ -8137,26 +8137,7 @@ eslint-plugin-cypress@^2.10.3:
dependencies:
globals "^11.12.0"
eslint-plugin-import@^2.20.2:
version "2.21.1"
resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.21.1.tgz#3398318e5e4abbd23395c4964ce61538705154c8"
integrity sha512-qYOOsgUv63vHof7BqbzuD+Ud34bXHxFJxntuAC1ZappFZXYbRIek3aJ7jc9i2dHDGDyZ/0zlO0cpioES265Lsw==
dependencies:
array-includes "^3.1.1"
array.prototype.flat "^1.2.3"
contains-path "^0.1.0"
debug "^2.6.9"
doctrine "1.5.0"
eslint-import-resolver-node "^0.3.3"
eslint-module-utils "^2.6.0"
has "^1.0.3"
minimatch "^3.0.4"
object.values "^1.1.1"
read-pkg-up "^2.0.0"
resolve "^1.17.0"
tsconfig-paths "^3.9.0"
eslint-plugin-import@^2.22.0:
eslint-plugin-import@^2.20.2, eslint-plugin-import@^2.22.0:
version "2.22.0"
resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e"
integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==
@@ -10185,6 +10166,16 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11"
micromatch "^3.1.10"
http-proxy-middleware@^0.19.1:
version "0.19.2"
resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.2.tgz#ee73dcc8348165afefe8de2ff717751d181608ee"
integrity sha512-aYk1rTKqLTus23X3L96LGNCGNgWpG4cG0XoZIT1GUPhhulEHX/QalnO6Vbo+WmKWi4AL2IidjuC0wZtbpg0yhQ==
dependencies:
http-proxy "^1.18.1"
is-glob "^4.0.0"
lodash "^4.17.11"
micromatch "^3.1.10"
http-proxy@^1.17.0:
version "1.18.0"
resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
@@ -13139,9 +13130,9 @@ ms@^2.0.0, ms@^2.1.1:
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
msw@^0.19.0:
version "0.19.4"
resolved "https://registry.npmjs.org/msw/-/msw-0.19.4.tgz#059026de0cc3303847c27d3aa0e98705a1033df6"
integrity sha512-rNfGgIuO0MyfN2F+FOHRqNd2LJIESaaMOJNNiNMcv/Be7Kdzz/ZmghfS/5zFy2SpHWladccqDYgo74JN+YlEyg==
version "0.19.5"
resolved "https://registry.npmjs.org/msw/-/msw-0.19.5.tgz#7d2a1a852ccf1644d3db6735d69fff6777aac33f"
integrity sha512-J5eQ++gDVZoHPC8gVXtWcakLjgmPipvFj/sEnlRV/WViXuiq2CamSqO3Wbh6H8bAmj+k2vUWCfcVT1HjMdKB2Q==
dependencies:
"@open-draft/until" "^1.0.0"
"@types/cookie" "^0.3.3"