Apply changes, update tests & change vault icon temporarilly
Signed-off-by: ivgo <ivgo@spreadgroup.com>
This commit is contained in:
@@ -17,31 +17,55 @@
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import express, { Router } from 'express';
|
||||
import { Duration } from 'luxon';
|
||||
import { VaultClient } from './vaultApi';
|
||||
import { runPeriodically } from './runPeriodically';
|
||||
import { TaskRunner, PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
|
||||
/**
|
||||
* Environment values needed by the VaultBuilder
|
||||
* @public
|
||||
*/
|
||||
export interface VaultEnvironment {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
scheduler: PluginTaskScheduler;
|
||||
}
|
||||
|
||||
export type VaultBuilderReturn = Promise<{
|
||||
/**
|
||||
* The object returned by the VaultBuilder.build() function
|
||||
* @public
|
||||
*/
|
||||
export type VaultBuilderReturn = {
|
||||
router: express.Router;
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Implementation for Vault. It creates the routing and initializes the backend
|
||||
* to allow the use of the Vault's frontend plugin.
|
||||
* @public
|
||||
*/
|
||||
export class VaultBuilder {
|
||||
private vaultTokenRefreshInterval: Duration = Duration.fromObject({
|
||||
minutes: 60,
|
||||
});
|
||||
// private vaultTokenRefreshInterval: Duration = Duration.fromObject({
|
||||
// minutes: 60,
|
||||
// });
|
||||
private vaultClient?: VaultClient;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the VaultBuilder.
|
||||
*
|
||||
* @param env - The backstage environment
|
||||
* @returns A new instance of the VaultBuilder
|
||||
*/
|
||||
static createBuilder(env: VaultEnvironment) {
|
||||
return new VaultBuilder(env);
|
||||
}
|
||||
constructor(protected readonly env: VaultEnvironment) {}
|
||||
|
||||
public async build(): VaultBuilderReturn {
|
||||
/**
|
||||
* Builds the routes for Vault.
|
||||
*
|
||||
* @returns The router configured for Vault
|
||||
*/
|
||||
public build(): VaultBuilderReturn {
|
||||
const { logger, config } = this.env;
|
||||
|
||||
logger.info('Initializing Vault backend');
|
||||
@@ -64,25 +88,46 @@ export class VaultBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the current vault client.
|
||||
*
|
||||
* @param vaultClient - The new Vault client
|
||||
* @returns
|
||||
*/
|
||||
public setVaultClient(vaultClient: VaultClient) {
|
||||
this.vaultClient = vaultClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setVaultTokenRefreshInterval(refreshInterval: Duration) {
|
||||
this.vaultTokenRefreshInterval = refreshInterval;
|
||||
return this;
|
||||
}
|
||||
|
||||
public enableTokenRenew() {
|
||||
runPeriodically(async () => {
|
||||
this.env.logger.info('Renewing Vault token');
|
||||
const vaultClient = this.vaultClient ?? new VaultClient(this.env);
|
||||
await this.renewToken(vaultClient);
|
||||
}, this.vaultTokenRefreshInterval.toMillis());
|
||||
/**
|
||||
* Enables the token renewal for Vault.
|
||||
*
|
||||
* @param schedule - The TaskRunner used to schedule the renewal, if not set, renewing hourly
|
||||
* @returns
|
||||
*/
|
||||
public async enableTokenRenew(schedule?: TaskRunner) {
|
||||
const taskRunner = schedule
|
||||
? schedule
|
||||
: this.env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { hours: 1 },
|
||||
timeout: { hours: 1 },
|
||||
});
|
||||
await taskRunner.run({
|
||||
id: 'refresh-vault-token',
|
||||
fn: async () => {
|
||||
this.env.logger.info('Renewing Vault token');
|
||||
const vaultClient = this.vaultClient ?? new VaultClient(this.env);
|
||||
await this.renewToken(vaultClient);
|
||||
},
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renews the token for vault using a defined client.
|
||||
*
|
||||
* @param vaultClient - The vault client used to renew the token
|
||||
*/
|
||||
protected async renewToken(vaultClient: VaultClient) {
|
||||
const result = await vaultClient.renewToken();
|
||||
if (!result) {
|
||||
@@ -92,6 +137,12 @@ export class VaultBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the backend routes for Vault.
|
||||
*
|
||||
* @param vaultClient - The Vault client used to list the secrets.
|
||||
* @returns The generated backend router
|
||||
*/
|
||||
protected buildRouter(vaultClient: VaultClient): express.Router {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
@@ -14,18 +14,32 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { getVoidLogger, DatabaseManager } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
|
||||
import { createRouter } from './router';
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
const databases = TestDatabases.create({
|
||||
ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
const knex = await databases.init('SQLITE_3');
|
||||
const databaseManager: Partial<DatabaseManager> = {
|
||||
forPlugin: (_: string) => ({
|
||||
getClient: async () => knex,
|
||||
}),
|
||||
};
|
||||
const manager = databaseManager as DatabaseManager;
|
||||
const scheduler = new TaskScheduler(manager, getVoidLogger());
|
||||
|
||||
const router = createRouter({
|
||||
logger: getVoidLogger(),
|
||||
config: new ConfigReader({
|
||||
vault: {
|
||||
@@ -33,6 +47,7 @@ describe('createRouter', () => {
|
||||
token: '1234567890',
|
||||
},
|
||||
}),
|
||||
scheduler: scheduler.forPlugin('vault'),
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
@@ -14,20 +14,31 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { VaultBuilder } from './VaultBuilder';
|
||||
|
||||
/**
|
||||
* Options for the router creation.
|
||||
* @public
|
||||
*/
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
scheduler: PluginTaskScheduler;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { router } = await VaultBuilder.createBuilder(options).build();
|
||||
/**
|
||||
* Creates the routes used for Vault.
|
||||
*
|
||||
* @param options - The options used for the Vault's router creation
|
||||
* @returns The router for Vault
|
||||
* @public
|
||||
*/
|
||||
export function createRouter(options: RouterOptions): express.Router {
|
||||
const { router } = VaultBuilder.createBuilder(options).build();
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Runs a function repeatedly, with a fixed wait between invocations.
|
||||
*
|
||||
* Supports async functions, and silently ignores exceptions and rejections.
|
||||
*
|
||||
* @param fn - The function to run. May return a Promise.
|
||||
* @param delayMs - The delay between a completed function invocation and the
|
||||
* next.
|
||||
* @returns A function that, when called, stops the invocation loop.
|
||||
*/
|
||||
export function runPeriodically(fn: () => any, delayMs: number): () => void {
|
||||
let cancel: () => void;
|
||||
let cancelled = false;
|
||||
const cancellationPromise = new Promise<void>(resolve => {
|
||||
cancel = () => {
|
||||
resolve();
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
const startRefresh = async () => {
|
||||
while (!cancelled) {
|
||||
try {
|
||||
await fn();
|
||||
} catch {
|
||||
// ignore intentionally
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
new Promise(resolve => setTimeout(resolve, delayMs)),
|
||||
cancellationPromise,
|
||||
]);
|
||||
}
|
||||
};
|
||||
startRefresh();
|
||||
|
||||
return cancel!;
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
DatabaseManager,
|
||||
getVoidLogger,
|
||||
} from '@backstage/backend-common';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
@@ -26,6 +28,8 @@ import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
enableCors: boolean;
|
||||
@@ -39,6 +43,20 @@ export async function createStandaloneApplication(
|
||||
const config = new ConfigReader({});
|
||||
const app = express();
|
||||
|
||||
const databases = TestDatabases.create({
|
||||
ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
|
||||
});
|
||||
const knex = await databases.init('SQLITE_3');
|
||||
const databaseManager: Partial<DatabaseManager> = {
|
||||
forPlugin: (_: string) => ({
|
||||
getClient: async () => knex,
|
||||
}),
|
||||
};
|
||||
const manager = databaseManager as DatabaseManager;
|
||||
const scheduler = new TaskScheduler(manager, getVoidLogger()).forPlugin(
|
||||
'vault',
|
||||
);
|
||||
|
||||
app.use(helmet());
|
||||
if (enableCors) {
|
||||
app.use(cors());
|
||||
@@ -46,7 +64,7 @@ export async function createStandaloneApplication(
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/', await createRouter({ logger, config }));
|
||||
app.use('/', createRouter({ logger, config, scheduler }));
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { Secret, VaultClient, VaultSecretList } from './vaultApi';
|
||||
import { VaultSecret, VaultClient, VaultSecretList } from './vaultApi';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('VaultApi', () => {
|
||||
@@ -43,7 +43,7 @@ describe('VaultApi', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockSecretsResult: Secret[] = [
|
||||
const mockSecretsResult: VaultSecret[] = [
|
||||
{
|
||||
name: 'secret::one',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`,
|
||||
|
||||
@@ -18,30 +18,61 @@ import { Config } from '@backstage/config';
|
||||
import fetch from 'cross-fetch';
|
||||
import { getVaultConfig, VaultConfig } from '../config';
|
||||
|
||||
/**
|
||||
* Object received as a response from the Vault API when fetching secrets
|
||||
* @public
|
||||
*/
|
||||
export type VaultSecretList = {
|
||||
data: {
|
||||
keys: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type Secret = {
|
||||
/**
|
||||
* Object containing the secret name and some links
|
||||
* @public
|
||||
*/
|
||||
export type VaultSecret = {
|
||||
name: string;
|
||||
showUrl: string;
|
||||
editUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Object received as response when the token is renewed using the Vault API
|
||||
* @public
|
||||
*/
|
||||
export type RenewTokenResponse = {
|
||||
auth: {
|
||||
client_token: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for the Vault API
|
||||
* @public
|
||||
*/
|
||||
export interface VaultApi {
|
||||
/**
|
||||
* Returns the URL to acces the Vault UI with the defined config.
|
||||
*/
|
||||
getFrontendSecretsUrl(): string;
|
||||
listSecrets(secretPath: string): Promise<Secret[]>;
|
||||
/**
|
||||
* Returns a list of secrets used to show in a table.
|
||||
* @param secretPath - The path where the secrets are stored in Vault
|
||||
*/
|
||||
listSecrets(secretPath: string): Promise<VaultSecret[]>;
|
||||
/**
|
||||
* Optional, to renew the token used to list the secrets. Returns true
|
||||
* if the action was successfull, false otherwise.
|
||||
*/
|
||||
renewToken?(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the Vault API to list secrets and renew the token if necessary
|
||||
* @public
|
||||
*/
|
||||
export class VaultClient implements VaultApi {
|
||||
private vaultConfig: VaultConfig;
|
||||
|
||||
@@ -81,7 +112,7 @@ export class VaultClient implements VaultApi {
|
||||
return `${this.vaultConfig.baseUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}`;
|
||||
}
|
||||
|
||||
async listSecrets(secretPath: string): Promise<Secret[]> {
|
||||
async listSecrets(secretPath: string): Promise<VaultSecret[]> {
|
||||
const listUrl =
|
||||
this.vaultConfig.kvVersion === 2
|
||||
? `v1/${this.vaultConfig.secretEngine}/metadata/${secretPath}`
|
||||
@@ -91,7 +122,7 @@ export class VaultClient implements VaultApi {
|
||||
return [];
|
||||
}
|
||||
|
||||
const secrets: Secret[] = [];
|
||||
const secrets: VaultSecret[] = [];
|
||||
await Promise.all(
|
||||
result.data.keys.map(async secret => {
|
||||
if (this.isFolder(secret)) {
|
||||
|
||||
Reference in New Issue
Block a user