diff --git a/.changeset/four-pears-swim.md b/.changeset/four-pears-swim.md new file mode 100644 index 0000000000..b091dad598 --- /dev/null +++ b/.changeset/four-pears-swim.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-vault-node': minor +--- + +Initial version of the `plugin-vault-node`` package. It contains the extension point definitions +for the vault backend, as well as some types that will be deprecated in the backend plugin. diff --git a/.changeset/great-baboons-allow.md b/.changeset/great-baboons-allow.md new file mode 100644 index 0000000000..47e410d8a4 --- /dev/null +++ b/.changeset/great-baboons-allow.md @@ -0,0 +1,33 @@ +--- +'@backstage/plugin-vault-backend': minor +--- + +Added support for the [new backend system](https://backstage.io/docs/backend-system/). + +In your `packages/backend/src/index.ts` make the following changes: + +```diff + import { createBackend } from '@backstage/backend-defaults'; + const backend = createBackend(); + // ... other feature additions ++ backend.add(import('@backstage/plugin-vault-backend'); + backend.start(); +``` + +If you use the new backend system, the token renewal task can be defined via configuration file: + +```diff +vault: + baseUrl: + token: + schedule: ++ frequency: ... ++ timeout: ... ++ # Other schedule options, such as scope or initialDelay +``` + +If the `schedule` is omitted or set to `false` no token renewal task will be scheduled. +If the value of `schedule` is set to `true` the renew will be scheduled hourly (the default). +In other cases (like in the diff above), the defined schedule will be used. + +**DEPRECATIONS**: The interface `VaultApi` and the type `VaultSecret` are now deprecated. Import them from `@backstage/plugin-vault-node`. diff --git a/plugins/vault-backend/README.md b/plugins/vault-backend/README.md index 8c70cfd24a..117bcaacbb 100644 --- a/plugins/vault-backend/README.md +++ b/plugins/vault-backend/README.md @@ -68,6 +68,9 @@ To get started, first you need a running instance of Vault. You can follow [this token: secretEngine: 'customSecretEngine' # Optional. By default it uses 'secrets'. Can be overwritten by the annotation of the entity kvVersion: # Optional. The K/V version that your instance is using. The available options are '1' or '2' + schedule: # Optional. If the token renewal is enabled this schedule will be used instead of the hourly one + frequency: { hours: 1 } + timeout: { hours: 1 } ``` 4. Get a `VAULT_TOKEN` with **LIST** permissions, as it's enough for the plugin. You can check [this tutorial](https://learn.hashicorp.com/tutorials/vault/tokens) for more info. @@ -80,6 +83,24 @@ To get started, first you need a running instance of Vault. You can follow [this } ``` +## New Backend System + +The Vault backend plugin has support for the [new backend system](https://backstage.io/docs/backend-system/), here's how you can set that up: + +In your `packages/backend/src/index.ts` make the following changes: + +```diff + import { createBackend } from '@backstage/backend-defaults'; + const backend = createBackend(); + // ... other feature additions ++ backend.add(import('@backstage/plugin-vault-backend'); + backend.start(); +``` + +The token renewal is enabled automatically in the new backend system depending on the `app-config.yaml`. If the `schedule` is not defined there, no +task will be executed. If you want to use the default renewal scheduler (which runs hourly), set `schedule: true`. In case you want a custom schedule +just use a configuration like the one set above. + ## Integration with the Catalog The plugin can be integrated into each Component in the catalog. To allow listing the available secrets a new annotation must be added to the `catalog-info.yaml`: @@ -122,7 +143,7 @@ That will overwrite the default secret engine from the configuration. ## Renew token -In a secure Vault instance, it's usual that the tokens are refreshed after some time. In order to always have a valid token to fetch the secrets, it might be necessary to execute a renew action after some time. By default this is deactivated, but it can be easily activated and configured to be executed periodically (hourly by default, but customizable by the user). In order to do that, modify your `src/plugins/vault.ts` file to look like this one: +In a secure Vault instance, it's usual that the tokens are refreshed after some time. In order to always have a valid token to fetch the secrets, it might be necessary to execute a renew action after some time. By default this is deactivated, but it can be easily activated and configured to be executed periodically (hourly by default, but customizable by the user within the app-config.yaml file). In order to do that, modify your `src/plugins/vault.ts` file to look like this one: ```typescript import { VaultBuilder } from '@backstage/plugin-vault-backend'; @@ -149,6 +170,8 @@ export default async function createPlugin( } ``` +If the `taskRunner` is not set when calling the `enableTokenRenew`, the plugin will automatically check what is set in the `app-config.yaml` file. Refer to [the new backend system setup](#new-backend-system) for more information about it. + ## Features - List the secrets present in a certain path diff --git a/plugins/vault-backend/api-report.md b/plugins/vault-backend/api-report.md index ebf9057b98..4002db5084 100644 --- a/plugins/vault-backend/api-report.md +++ b/plugins/vault-backend/api-report.md @@ -3,9 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; @@ -22,7 +24,7 @@ export interface RouterOptions { scheduler: PluginTaskScheduler; } -// @public +// @public @deprecated export interface VaultApi { getFrontendSecretsUrl(): string; listSecrets( @@ -69,12 +71,16 @@ export interface VaultEnvironment { // (undocumented) config: Config; // (undocumented) - logger: Logger; + logger: LoggerService; // (undocumented) scheduler: PluginTaskScheduler; } // @public +const vaultPlugin: () => BackendFeature; +export default vaultPlugin; + +// @public @deprecated export type VaultSecret = { name: string; path: string; diff --git a/plugins/vault-backend/config.d.ts b/plugins/vault-backend/config.d.ts index a30999b0cb..58369e638d 100644 --- a/plugins/vault-backend/config.d.ts +++ b/plugins/vault-backend/config.d.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks'; /** Configuration for the Vault plugin */ export interface Config { @@ -44,5 +45,11 @@ export interface Config { * The version of the K/V API. Defaults to `2`. */ kvVersion?: 1 | 2; + + /** + * If set to true, the default schedule (hourly) will be used. If a + * different schedule is set, this will be used instead. + * */ + schedule?: TaskScheduleDefinitionConfig | boolean; }; } diff --git a/plugins/vault-backend/package.json b/plugins/vault-backend/package.json index 883bb5281b..7e061c8cc3 100644 --- a/plugins/vault-backend/package.json +++ b/plugins/vault-backend/package.json @@ -34,9 +34,11 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/plugin-vault-node": "workspace:^", "@types/express": "*", "compression": "^1.7.4", "cors": "^2.8.5", diff --git a/plugins/vault-backend/src/index.ts b/plugins/vault-backend/src/index.ts index 4c74fd051f..c5fea00642 100644 --- a/plugins/vault-backend/src/index.ts +++ b/plugins/vault-backend/src/index.ts @@ -17,3 +17,4 @@ export * from './service/router'; export * from './service/VaultBuilder'; export * from './service/vaultApi'; +export { vaultPlugin as default } from './service/plugin'; diff --git a/plugins/vault-backend/src/service/VaultBuilder.ts b/plugins/vault-backend/src/service/VaultBuilder.ts index 9856476c20..471064392f 100644 --- a/plugins/vault-backend/src/service/VaultBuilder.ts +++ b/plugins/vault-backend/src/service/VaultBuilder.ts @@ -16,11 +16,17 @@ import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import express from 'express'; import Router from 'express-promise-router'; import { VaultApi, VaultClient } from './vaultApi'; -import { TaskRunner, PluginTaskScheduler } from '@backstage/backend-tasks'; +import { + PluginTaskScheduler, + TaskRunner, + TaskScheduleDefinition, + TaskScheduleDefinitionConfig, + readTaskScheduleDefinitionFromConfig, +} from '@backstage/backend-tasks'; import { errorHandler } from '@backstage/backend-common'; /** @@ -28,7 +34,7 @@ import { errorHandler } from '@backstage/backend-common'; * @public */ export interface VaultEnvironment { - logger: Logger; + logger: LoggerService; config: Config; scheduler: PluginTaskScheduler; } @@ -101,18 +107,16 @@ export class VaultBuilder { } /** - * Enables the token renewal for Vault. + * Enables the token renewal for Vault. The schedule if configured in the app-config.yaml file. + * If not set, the renewal is executed hourly * - * @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 }, - }); + : this.env.scheduler.createScheduledTaskRunner(this.getConfigSchedule()); + await taskRunner.run({ id: 'refresh-vault-token', fn: async () => { @@ -124,6 +128,24 @@ export class VaultBuilder { return this; } + private getConfigSchedule(): TaskScheduleDefinition { + const schedule = this.env.config.getOptional< + TaskScheduleDefinitionConfig | boolean + >('vault.schedule'); + + const scheduleCfg = + schedule !== undefined && schedule !== false + ? { + frequency: { hours: 1 }, + timeout: { hours: 1 }, + } + : readTaskScheduleDefinitionFromConfig( + this.env.config.getConfig('vault.schedule'), + ); + + return scheduleCfg; + } + /** * Builds the backend routes for Vault. * diff --git a/plugins/vault-backend/src/service/plugin.test.tsx b/plugins/vault-backend/src/service/plugin.test.tsx new file mode 100644 index 0000000000..cc9fdaa0a7 --- /dev/null +++ b/plugins/vault-backend/src/service/plugin.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright 2021 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. + */ +import { vaultPlugin } from './plugin'; + +describe('vault', () => { + it('should export the vault plugin', () => { + expect(vaultPlugin).toBeDefined(); + }); +}); diff --git a/plugins/vault-backend/src/service/plugin.ts b/plugins/vault-backend/src/service/plugin.ts new file mode 100644 index 0000000000..cf9d2cf87b --- /dev/null +++ b/plugins/vault-backend/src/service/plugin.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { VaultApi, vaultExtensionPoint } from '@backstage/plugin-vault-node'; +import { VaultBuilder } from './VaultBuilder'; +import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks'; + +/** + * Vault backend plugin + * + * @public + */ +export const vaultPlugin = createBackendPlugin({ + pluginId: 'vault', + register(env) { + let client: VaultApi | undefined; + + env.registerExtensionPoint(vaultExtensionPoint, { + setClient(vaultClient) { + if (client) { + throw new Error('The vault client has been already set'); + } + client = vaultClient; + }, + }); + + env.registerInit({ + deps: { + logger: coreServices.logger, + config: coreServices.rootConfig, + scheduler: coreServices.scheduler, + httpRouter: coreServices.httpRouter, + }, + async init({ logger, config, scheduler, httpRouter }) { + let builder = VaultBuilder.createBuilder({ + logger, + config, + scheduler, + }); + + if (client) { + builder = builder.setVaultClient(client); + } + + const scheduleCfg = config.getOptional< + boolean | TaskScheduleDefinitionConfig + >('vault.schedule'); + if (scheduleCfg !== undefined && scheduleCfg !== false) { + builder = await builder.enableTokenRenew(); + } + + const { router } = builder.build(); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/plugins/vault-backend/src/service/vaultApi.ts b/plugins/vault-backend/src/service/vaultApi.ts index 7c3ccc1d00..789cdeeda5 100644 --- a/plugins/vault-backend/src/service/vaultApi.ts +++ b/plugins/vault-backend/src/service/vaultApi.ts @@ -33,6 +33,7 @@ export type VaultSecretList = { /** * Object containing the secret name and some links * @public + * @deprecated Use the interface from `@backstage/plugin-vault-node` */ export type VaultSecret = { name: string; @@ -53,6 +54,7 @@ type RenewTokenResponse = { /** * Interface for the Vault API * @public + * @deprecated Use the interface from `@backstage/plugin-vault-node` */ export interface VaultApi { /** diff --git a/plugins/vault-node/.eslintrc.js b/plugins/vault-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/vault-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/vault-node/README.md b/plugins/vault-node/README.md new file mode 100644 index 0000000000..5f777e792b --- /dev/null +++ b/plugins/vault-node/README.md @@ -0,0 +1,3 @@ +# @backstage/plugin-vault-node + +Houses types and utilities for building the vault modules diff --git a/plugins/vault-node/api-report.md b/plugins/vault-node/api-report.md new file mode 100644 index 0000000000..d0e0a29f8f --- /dev/null +++ b/plugins/vault-node/api-report.md @@ -0,0 +1,38 @@ +## API Report File for "@backstage/plugin-vault-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ExtensionPoint } from '@backstage/backend-plugin-api'; + +// @public +export interface VaultApi { + getFrontendSecretsUrl(): string; + listSecrets( + secretPath: string, + options?: { + secretEngine?: string; + }, + ): Promise; + renewToken?(): Promise; +} + +// @public +export interface VaultExtensionPoint { + // (undocumented) + setClient(vaultClient: VaultApi): void; +} + +// @public +export const vaultExtensionPoint: ExtensionPoint; + +// @public +export type VaultSecret = { + name: string; + path: string; + showUrl: string; + editUrl: string; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/vault-node/catalog-info.yaml b/plugins/vault-node/catalog-info.yaml new file mode 100644 index 0000000000..6b6c8b84ad --- /dev/null +++ b/plugins/vault-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-vault-node + title: '@backstage/plugin-vault-node' + description: Node.js library for the vault plugin +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/plugins/vault-node/package.json b/plugins/vault-node/package.json new file mode 100644 index 0000000000..a0a821680c --- /dev/null +++ b/plugins/vault-node/package.json @@ -0,0 +1,33 @@ +{ + "name": "@backstage/plugin-vault-node", + "description": "Node.js library for the vault plugin", + "version": "0.0.0", + "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" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/vault-node/src/api/index.ts b/plugins/vault-node/src/api/index.ts new file mode 100644 index 0000000000..92cb965d75 --- /dev/null +++ b/plugins/vault-node/src/api/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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. + */ +export * from './types'; diff --git a/plugins/vault-node/src/api/types.ts b/plugins/vault-node/src/api/types.ts new file mode 100644 index 0000000000..9969fb001b --- /dev/null +++ b/plugins/vault-node/src/api/types.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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. + */ + +/** + * Object containing the secret name and some links + * @public + */ +export type VaultSecret = { + name: string; + path: string; + showUrl: string; + editUrl: string; +}; + +/** + * Interface for the Vault API + * @public + */ +export interface VaultApi { + /** + * Returns the URL to access the Vault UI with the defined config. + */ + getFrontendSecretsUrl(): string; + + /** + * Returns a list of secrets used to show in a table. + * @param secretPath - The path where the secrets are stored in Vault + * @param options - Additional options to be passed to the Vault API, allows to override vault default settings in app config file + */ + listSecrets( + secretPath: string, + options?: { + secretEngine?: string; + }, + ): Promise; + + /** + * Optional, to renew the token used to list the secrets. Throws an + * error if the token renewal went wrong. + */ + renewToken?(): Promise; +} diff --git a/plugins/vault-node/src/extensions.ts b/plugins/vault-node/src/extensions.ts new file mode 100644 index 0000000000..efe4ba6223 --- /dev/null +++ b/plugins/vault-node/src/extensions.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2023 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. + */ + +import { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { VaultApi } from './api'; + +/** + * Extension point for vault. + * + * @public + */ +export interface VaultExtensionPoint { + setClient(vaultClient: VaultApi): void; +} + +/** + * Extension point for vault. + * + * @public + */ +export const vaultExtensionPoint = createExtensionPoint({ + id: 'vault.configuration', +}); diff --git a/plugins/vault-node/src/index.ts b/plugins/vault-node/src/index.ts new file mode 100644 index 0000000000..070f2cf64d --- /dev/null +++ b/plugins/vault-node/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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. + */ +export { type VaultExtensionPoint, vaultExtensionPoint } from './extensions'; +export type { VaultApi, VaultSecret } from './api'; diff --git a/plugins/vault-node/src/setupTests.ts b/plugins/vault-node/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/vault-node/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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. + */ +export {}; diff --git a/yarn.lock b/yarn.lock index ccbafcc1bc..b1b46444a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9822,11 +9822,13 @@ __metadata: resolution: "@backstage/plugin-vault-backend@workspace:plugins/vault-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/plugin-vault-node": "workspace:^" "@types/compression": ^1.7.2 "@types/express": "*" "@types/supertest": ^2.0.8 @@ -9844,6 +9846,15 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-vault-node@workspace:^, @backstage/plugin-vault-node@workspace:plugins/vault-node": + version: 0.0.0-use.local + resolution: "@backstage/plugin-vault-node@workspace:plugins/vault-node" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-vault@workspace:plugins/vault": version: 0.0.0-use.local resolution: "@backstage/plugin-vault@workspace:plugins/vault"