Merge branch 'master' into k8s-backend-skipTLSVerify-configurable

Signed-off-by: Travis Truman <trumant@gmail.com>
This commit is contained in:
Travis Truman
2021-04-15 09:30:05 -04:00
73 changed files with 1150 additions and 727 deletions
+1 -2
View File
@@ -55,8 +55,7 @@ proxy:
target: 'https://api.bitrise.io/v0.1'
allowedMethods: ['GET']
headers:
Authorization:
$env: BITRISE_AUTH_TOKEN
Authorization: ${BITRISE_AUTH_TOKEN}
```
Learn on https://devcenter.bitrise.io/api/authentication how to create a new Bitrise token.
+1 -1
View File
@@ -42,7 +42,7 @@ export class CatalogClientWrapper implements CatalogApi {
}
async getLocationById(
id: String,
id: string,
options?: CatalogRequestOptions,
): Promise<Location | undefined> {
return await this.client.getLocationById(id, {
+1 -2
View File
@@ -43,8 +43,7 @@ proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
headers:
Circle-Token:
$env: CIRCLECI_AUTH_TOKEN
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
```
5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token)
+1 -3
View File
@@ -50,9 +50,7 @@ proxy:
target: https://app.fossa.io/api
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'token <your-fossa-api-token>'
$env: FOSSA_AUTH_HEADER
Authorization: token ${FOSSA_API_TOKEN}
# if you have a fossa organization, configure your id here
fossa:
+2 -4
View File
@@ -29,15 +29,13 @@ proxy:
target: 'http://localhost:8080' # your Jenkins URL
changeOrigin: true
headers:
Authorization:
$env: JENKINS_BASIC_AUTH_HEADER
Authorization: Basic ${JENKINS_BASIC_AUTH_HEADER}
```
4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password). Here user is the name of the user created in Jenkins.
```shell
HEADER=$(echo -n user:api-token | base64)
export JENKINS_BASIC_AUTH_HEADER="Basic $HEADER"
export JENKINS_BASIC_AUTH_HEADER=$(echo -n user:api-token | base64)
```
5. Run app with `yarn start`
@@ -21,7 +21,7 @@ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator
export class KubernetesAuthTranslatorGenerator {
static getKubernetesAuthTranslatorInstance(
authProvider: String,
authProvider: string,
): KubernetesAuthTranslator {
switch (authProvider) {
case 'google': {
@@ -34,8 +34,8 @@ const mockFetch = (mock: jest.Mock) => {
};
function generateMockResourcesAndErrors(
serviceId: String,
clusterName: String,
serviceId: string,
clusterName: string,
) {
if (clusterName === 'empty-cluster') {
return {
+1 -2
View File
@@ -15,8 +15,7 @@ proxy:
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
headers:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
```
In your production deployment of Backstage, you would also need to ensure that
+1 -2
View File
@@ -8,8 +8,7 @@ The following values are read from the configuration file.
```yaml
rollbar:
accountToken:
$env: ROLLBAR_ACCOUNT_TOKEN
accountToken: ${ROLLBAR_ACCOUNT_TOKEN}
```
_NOTE: The `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read
+1 -2
View File
@@ -45,8 +45,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
rollbar:
organization: organization-name
# used by rollbar-backend
accountToken:
$env: ROLLBAR_ACCOUNT_TOKEN
accountToken: ${ROLLBAR_ACCOUNT_TOKEN}
```
6. Annotate entities with the rollbar project slug
@@ -50,6 +50,7 @@ spec:
values:
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
destination: '{{ parseRepoUrl parameters.repoUrl }}'
- id: fetch-docs
name: Fetch Docs
@@ -2,6 +2,7 @@ apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: {{cookiecutter.name | jsonify}}
github.com/project-slug: {{cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}}
spec:
type: website
lifecycle: experimental
@@ -30,8 +30,14 @@ export const getRepoSourceDirectory = (
}
return workspacePath;
};
export type RepoSpec = {
repo: string;
host: string;
owner: string;
organization?: string;
};
export const parseRepoUrl = (repoUrl: string) => {
export const parseRepoUrl = (repoUrl: string): RepoSpec => {
let parsed;
try {
parsed = new URL(`https://${repoUrl}`);
@@ -55,7 +61,7 @@ export const parseRepoUrl = (repoUrl: string) => {
);
}
const organization = parsed.searchParams.get('organization');
const organization = parsed.searchParams.get('organization') ?? undefined;
return { host, owner, repo, organization };
};
@@ -24,6 +24,7 @@ import { ConfigReader, JsonObject } from '@backstage/config';
import { StorageTaskBroker } from './StorageTaskBroker';
import { DatabaseTaskStore } from './DatabaseTaskStore';
import { createTemplateAction, TemplateActionRegistry } from '../actions';
import { RepoSpec } from '../actions/builtin/publish/util';
async function createStore(): Promise<DatabaseTaskStore> {
const manager = SingleConnectionDatabaseManager.fromConfig(
@@ -41,20 +42,24 @@ async function createStore(): Promise<DatabaseTaskStore> {
describe('TaskWorker', () => {
let storage: DatabaseTaskStore;
let actionRegistry = new TemplateActionRegistry();
beforeAll(async () => {
storage = await createStore();
});
const logger = getVoidLogger();
const actionRegistry = new TemplateActionRegistry();
actionRegistry.register({
id: 'test-action',
handler: async ctx => {
ctx.output('testOutput', 'winning');
},
beforeEach(() => {
actionRegistry = new TemplateActionRegistry();
actionRegistry.register({
id: 'test-action',
handler: async ctx => {
ctx.output('testOutput', 'winning');
},
});
});
const logger = getVoidLogger();
it('should fail when action does not exist', async () => {
const broker = new StorageTaskBroker(storage, logger);
const taskWorker = new TaskWorker({
@@ -166,4 +171,194 @@ describe('TaskWorker', () => {
const event = events.find(e => e.type === 'completion');
expect((event?.body?.output as JsonObject).result).toBe('winning');
});
it('should parse strings as objects if possible', async () => {
const inputAction = createTemplateAction<{
address: { line1: string };
list: string[];
address2: string;
}>({
id: 'test-input',
schema: {
input: {
type: 'object',
required: ['address'],
properties: {
address: {
title: 'address',
description: 'Enter name',
type: 'object',
properties: {
line1: {
type: 'string',
},
},
},
address2: {
type: 'string',
},
list: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
async handler(ctx) {
if (ctx.input.list.length !== 1) {
throw new Error(
`expected list to have length "1" got ${ctx.input.list.length}`,
);
}
if (ctx.input.address.line1 !== 'line 1') {
throw new Error(
`expected address.line1 to be "line 1" got ${ctx.input.address.line1}`,
);
}
if (ctx.input.address2 !== '{"not valid"}') {
throw new Error(
`expected address2 to be "{"not valid"}" got ${ctx.input.address2}`,
);
}
ctx.output('address', ctx.input.address.line1);
},
});
actionRegistry.register(inputAction);
const broker = new StorageTaskBroker(storage, logger);
const taskWorker = new TaskWorker({
logger,
workingDirectory: os.tmpdir(),
actionRegistry,
taskBroker: broker,
});
const { taskId } = await broker.dispatch({
steps: [
{
id: 'test-input',
name: 'test-input',
action: 'test-input',
input: {
address: JSON.stringify({ line1: 'line 1' }),
list: JSON.stringify(['hey!']),
address2: '{"not valid"}',
},
},
],
output: {
result: '{{ steps.test-input.output.address }}',
},
values: {},
});
const task = await broker.claim();
await taskWorker.runOneTask(task);
const { events } = await storage.listEvents({ taskId });
const event = events.find(e => e.type === 'completion');
expect((event?.body?.output as JsonObject).result).toBe('line 1');
});
// TODO(blam): Can delete this test when we make the helpers a public API
it('should provide a repoUrlParse helper for the templates', async () => {
const inputAction = createTemplateAction<{
destination: RepoSpec;
}>({
id: 'test-input',
schema: {
input: {
type: 'object',
required: ['destination'],
properties: {
destination: {
title: 'destination',
type: 'object',
properties: {
repo: {
type: 'string',
},
host: {
type: 'string',
},
owner: {
type: 'string',
},
organization: {
type: 'string',
},
},
},
},
},
},
async handler(ctx) {
ctx.output('host', ctx.input.destination.host);
ctx.output('repo', ctx.input.destination.repo);
ctx.output('owner', ctx.input.destination.owner);
if (ctx.input.destination.host !== 'github.com') {
throw new Error(
`expected host to be "github.com" got ${ctx.input.destination.host}`,
);
}
if (ctx.input.destination.repo !== 'repo') {
throw new Error(
`expected repo to be "repo" got ${ctx.input.destination.repo}`,
);
}
if (ctx.input.destination.owner !== 'owner') {
throw new Error(
`expected repo to be "owner" got ${ctx.input.destination.owner}`,
);
}
},
});
actionRegistry.register(inputAction);
const broker = new StorageTaskBroker(storage, logger);
const taskWorker = new TaskWorker({
logger,
workingDirectory: os.tmpdir(),
actionRegistry,
taskBroker: broker,
});
const { taskId } = await broker.dispatch({
steps: [
{
id: 'test-input',
name: 'test-input',
action: 'test-input',
input: {
destination: '{{ parseRepoUrl parameters.repoUrl }}',
},
},
],
output: {
host: '{{ steps.test-input.output.host }}',
repo: '{{ steps.test-input.output.repo }}',
owner: '{{ steps.test-input.output.owner }}',
},
values: {
repoUrl: 'github.com?repo=repo&owner=owner',
},
});
const task = await broker.claim();
await taskWorker.runOneTask(task);
const { events } = await storage.listEvents({ taskId });
const event = events.find(e => e.type === 'completion');
expect((event?.body?.output as JsonObject).host).toBe('github.com');
expect((event?.body?.output as JsonObject).repo).toBe('repo');
expect((event?.body?.output as JsonObject).owner).toBe('owner');
});
});
@@ -23,8 +23,9 @@ import { TaskBroker, Task } from './types';
import fs from 'fs-extra';
import path from 'path';
import { TemplateActionRegistry } from '../actions/TemplateActionRegistry';
import * as handlebars from 'handlebars';
import * as Handlebars from 'handlebars';
import { InputError } from '@backstage/errors';
import { parseRepoUrl } from '../actions/builtin/publish/util';
type Options = {
logger: Logger;
@@ -34,7 +35,20 @@ type Options = {
};
export class TaskWorker {
constructor(private readonly options: Options) {}
private readonly handlebars: typeof Handlebars;
constructor(private readonly options: Options) {
this.handlebars = Handlebars.create();
// TODO(blam): this should be a public facing API but it's a little
// scary right now, so we're going to lock it off like the component API is
// in the frontend until we can work out a nice way to do it.
this.handlebars.registerHelper('parseRepoUrl', repoUrl => {
return JSON.stringify(parseRepoUrl(repoUrl));
});
this.handlebars.registerHelper('json', obj => JSON.stringify(obj));
}
start() {
(async () => {
@@ -102,13 +116,29 @@ export class TaskWorker {
step.input &&
JSON.parse(JSON.stringify(step.input), (_key, value) => {
if (typeof value === 'string') {
return handlebars.compile(value, {
const templated = this.handlebars.compile(value, {
noEscape: true,
strict: true,
data: false,
preventIndent: true,
})(templateCtx);
// If it smells like a JSON object then give it a parse as an object and if it fails return the string
if (
(templated.startsWith('{') && templated.endsWith('}')) ||
(templated.startsWith('[') && templated.endsWith(']'))
) {
try {
// Don't recursively JSON parse the values of this string.
// Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else
return JSON.parse(templated);
} catch {
return templated;
}
}
return templated;
}
return value;
});
@@ -171,7 +201,7 @@ export class TaskWorker {
JSON.stringify(task.spec.output),
(_key, value) => {
if (typeof value === 'string') {
return handlebars.compile(value, {
return this.handlebars.compile(value, {
noEscape: true,
strict: true,
data: false,
+1 -3
View File
@@ -70,9 +70,7 @@ proxy:
target: https://sentry.io/api/
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'Bearer <your-sentry-token>'
$env: SENTRY_TOKEN
Authorization: Bearer ${SENTRY_TOKEN}
sentry:
organization: <your-organization>
+11 -13
View File
@@ -54,10 +54,9 @@ proxy:
target: https://sonarcloud.io/api
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'Basic base64("<api-key>:")' <-- note the trailing ':'
# Example: Basic bXktYXBpLWtleTo=
$env: SONARQUBE_AUTH_HEADER
Authorization: Basic ${SONARQUBE_AUTH}
# Content: 'base64("<api-key>:")' <-- note the trailing ':'
# Example: bXktYXBpLWtleTo=
```
**SonarQube**
@@ -70,20 +69,19 @@ proxy:
target: https://your.sonarqube.instance.com/api
allowedMethods: ['GET']
headers:
Authorization:
# Environmental variable: SONARQUBE_AUTH_HEADER
# Value: 'Basic base64("<sonar-auth-token>:")'
# Encode the "<sonar-auth-token>:" string using base64 encoder.
# Note the trailing colon (:) at the end of the token.
# Example environmental config: SONARQUBE_AUTH_HEADER=Basic bXktYXBpLWtleTo=
# Fetch the sonar-auth-token from https://sonarcloud.io/account/security/
$env: SONARQUBE_AUTH_HEADER
Authorization: Basic ${SONARQUBE_AUTH}
# Environmental variable: SONARQUBE_AUTH
# Value: 'base64("<sonar-auth-token>:")'
# Encode the "<sonar-auth-token>:" string using base64 encoder.
# Note the trailing colon (:) at the end of the token.
# Example environmental config: SONARQUBE_AUTH=bXktYXBpLWtleTo=
# Fetch the sonar-auth-token from https://sonarcloud.io/account/security/
sonarQube:
baseUrl: https://your.sonarqube.instance.com
```
5. Get and provide `SONARQUBE_AUTH_HEADER` as env variable (https://sonarcloud.io/account/security or https://docs.sonarqube.org/latest/user-guide/user-token/)
5. Get and provide `SONARQUBE_AUTH` as an env variable (https://sonarcloud.io/account/security or https://docs.sonarqube.org/latest/user-guide/user-token/)
6. Run the following commands in the root folder of the project to install and compile the changes.
+3 -5
View File
@@ -50,7 +50,7 @@ import {
In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST Generic.
To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST Generic.
You can now copy the URL to notify: `<SPLUNK_ON_CALL_REST_ENDPOINT>/$routing_key`
In `app-config.yaml`:
@@ -69,10 +69,8 @@ proxy:
'/splunk-on-call':
target: https://api.victorops.com/api-public
headers:
X-VO-Api-Id:
$env: SPLUNK_ON_CALL_API_ID
X-VO-Api-Key:
$env: SPLUNK_ON_CALL_API_KEY
X-VO-Api-Id: ${SPLUNK_ON_CALL_API_ID}
X-VO-Api-Key: ${SPLUNK_ON_CALL_API_KEY}
```
In addition, to make certain API calls (trigger-resolve-acknowledge an incident) you need to add the `PATCH` method to the backend `cors` methods list: `[GET, POST, PUT, DELETE, PATCH]`.
@@ -14,7 +14,13 @@
* limitations under the License.
*/
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
ApiProvider,
ApiRegistry,
ConfigApi,
configApiRef,
ConfigReader,
} from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
@@ -26,7 +32,16 @@ describe('TechDocs Home', () => {
getEntities: async () => ({ items: [] }),
};
const apiRegistry = ApiRegistry.with(catalogApiRef, catalogApi);
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
},
});
const apiRegistry = ApiRegistry.with(catalogApiRef, catalogApi).with(
configApiRef,
configApi,
);
it('should render a TechDocs home page', async () => {
await renderInTestApp(
@@ -38,7 +53,7 @@ describe('TechDocs Home', () => {
// Header
expect(await screen.findByText('Documentation')).toBeInTheDocument();
expect(
await screen.findByText(/Documentation available in Backstage/i),
await screen.findByText(/Documentation available in My Company/i),
).toBeInTheDocument();
// Explore Content
@@ -21,6 +21,8 @@ import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import {
CodeSnippet,
ConfigApi,
configApiRef,
Content,
Header,
HeaderTabs,
@@ -36,6 +38,7 @@ import { OwnedContent } from './OwnedContent';
export const TechDocsHome = () => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const catalogApi: CatalogApi = useApi(catalogApiRef);
const configApi: ConfigApi = useApi(configApiRef);
const tabs = [{ label: 'Overview' }, { label: 'Owned Documents' }];
@@ -46,13 +49,14 @@ export const TechDocsHome = () => {
});
});
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
if (loading) {
return (
<Page themeId="documentation">
<Header
title="Documentation"
subtitle="Documentation available in Backstage"
/>
<Header title="Documentation" subtitle={generatedSubtitle} />
<Content>
<Progress />
</Content>
@@ -63,10 +67,7 @@ export const TechDocsHome = () => {
if (error) {
return (
<Page themeId="documentation">
<Header
title="Documentation"
subtitle="Documentation available in Backstage"
/>
<Header title="Documentation" subtitle={generatedSubtitle} />
<Content>
<WarningPanel
severity="error"
@@ -81,10 +82,7 @@ export const TechDocsHome = () => {
return (
<Page themeId="documentation">
<Header
title="Documentation"
subtitle="Documentation available in Backstage"
/>
<Header title="Documentation" subtitle={generatedSubtitle} />
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
@@ -22,10 +22,8 @@ const EXAMPLE = `auth:
providers:
google:
development:
clientId:
$env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
$env: AUTH_GOOGLE_CLIENT_SECRET
clientId: \${AUTH_GOOGLE_CLIENT_ID}
clientSecret: \${AUTH_GOOGLE_CLIENT_SECRET}
`;
export const EmptyProviders = () => (