diff --git a/.changeset/cold-coins-tickle.md b/.changeset/cold-coins-tickle.md new file mode 100644 index 0000000000..301a9d0a84 --- /dev/null +++ b/.changeset/cold-coins-tickle.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added an option to be able to trigger refreshes on entities based on a prestored arbitrary key. + +The UrlReaderProcessor, FileReaderProcessor got updated to store the absolute URL of the catalog file as a refresh key. In the format of `:` +The PlaceholderProcessor got updated to store the resolverValues as refreshKeys for the entities. + +The custom resolvers will need to be updated to pass in a `CatalogProcessorEmit` function as parameter and they should be updated to emit their refresh processingResults. You can see the updated resolvers in the `PlaceholderProcessor.ts` + +```ts + // yamlPlaceholderResolver + ... + const { content, url } = await readTextLocation(params); + + params.emit(processingResult.refresh(`url:${url}`)); + ... +``` diff --git a/.changeset/cuddly-comics-pump.md b/.changeset/cuddly-comics-pump.md new file mode 100644 index 0000000000..6d224bd34c --- /dev/null +++ b/.changeset/cuddly-comics-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fixed that adding more than one `allowedOwner` or `allowedRepo` in the template config will now still set the first value as default in the initial form state of `RepoUrlPicker`. diff --git a/.changeset/fifty-cars-compare.md b/.changeset/fifty-cars-compare.md new file mode 100644 index 0000000000..86115a4b02 --- /dev/null +++ b/.changeset/fifty-cars-compare.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-kubernetes-backend': minor +--- + +Add `localKubectlProxy` cluster locator method to make local development simpler to setup. + +Consolidated no-op server side auth decorators. +The following Kubernetes auth decorators are now one class (`ServerSideKubernetesAuthProvider`): + +- `AwsKubernetesAuthProvider` +- `AzureKubernetesAuthProvider` +- `ServiceAccountKubernetesAuthProvider` diff --git a/.changeset/nervous-hounds-matter.md b/.changeset/nervous-hounds-matter.md new file mode 100644 index 0000000000..0f60b7ae2c --- /dev/null +++ b/.changeset/nervous-hounds-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +contextMenu prop passed through to from the component diff --git a/.changeset/renovate-e091137.md b/.changeset/renovate-e091137.md new file mode 100644 index 0000000000..a5f53a1ec5 --- /dev/null +++ b/.changeset/renovate-e091137.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Updated dependency `@octokit/webhooks` to `^10.0.0`. diff --git a/.changeset/rich-steaks-juggle.md b/.changeset/rich-steaks-juggle.md new file mode 100644 index 0000000000..5879f2dace --- /dev/null +++ b/.changeset/rich-steaks-juggle.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-tech-insights-node': patch +'@backstage/plugin-tech-insights-backend': patch +--- + +The `FactRetriever` model has been extended by adding optional title and description fields, allowing you to display them in the UI. diff --git a/.changeset/spicy-walls-repair.md b/.changeset/spicy-walls-repair.md new file mode 100644 index 0000000000..36187600c7 --- /dev/null +++ b/.changeset/spicy-walls-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add new config option for okta auth server and IDP diff --git a/.github/workflows/pr-review-comment-trigger.yaml b/.github/workflows/pr-review-comment-trigger.yaml index 25475a03dc..67e4c5abba 100644 --- a/.github/workflows/pr-review-comment-trigger.yaml +++ b/.github/workflows/pr-review-comment-trigger.yaml @@ -16,4 +16,13 @@ jobs: if: github.repository == 'backstage/backstage' && github.event.comment.user.id == github.event.pull_request.user.id steps: - - run: echo "This PR needs another review" + - name: Save PR number + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + mkdir -p ./pr + echo $PR_NUMBER > ./pr/pr_number + - uses: actions/upload-artifact@v3 + with: + name: pr_number-${{ github.event.pull_request.number }} + path: pr/ diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index 37cdea5aa9..fac7fd2fc4 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -15,10 +15,30 @@ jobs: if: github.repository == 'backstage/backstage' && github.event.workflow_run.conclusion == 'success' steps: + # Inspired by https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow + - name: Read PR Number + id: pr-number + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + const [artifact] = allArtifacts.data.artifacts.filter(artifact => artifact.name.startsWith('pr_number-')) + if (!artifact) { + throw new Error('No PR Number artifact available') + } + + const prNumber = artifact.name.slice('pr_number-'.length) + console.log(`::set-output name=pr-number::${prNumber}`); + - uses: backstage/actions/re-review@v0.5.3 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} project-id: PVT_kwDOBFKqdc02LQ - issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} + issue-number: ${{ steps.pr-number.outputs.pr-number }} diff --git a/ADOPTERS.md b/ADOPTERS.md index b3e27472f9..941f9ef969 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -198,3 +198,4 @@ _You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKi | [Backbase](https://backbase.com) | [Nicolas Torres](mailto:ntorres.dev@gmail.com) | To catalog components and teams | | [https://www.clear.sale](https://www.clear.sale) | [Paulo Baima](mailto:paulo.filho@clear.sale) | Central Hub for all the company modules, enabling the track of ownership of components and resources and how they relate to each other. | | [www.leroymerlin.com.br](https://www.leroymerlin.com.br) | [Rodrigo Franzoni](mailto:rfranzoni@leroymerlin.com.br) | Our engineers use the Backstage to solve problems around ownership and visibility of our applications, access service catalog, documentation, observability and infrastructure. | +| [Intility](https://intility.no/en/) | [@daniwk](https://github.com/daniwk) | We are creating a developer portal powered by Backstage, with software catalog, documentation, templates and integrations to our infrastructure and internal tools. | diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index 35394094f8..e4fc8bfb45 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -41,6 +41,8 @@ auth: clientId: ${AUTH_OKTA_CLIENT_ID} clientSecret: ${AUTH_OKTA_CLIENT_SECRET} audience: ${AUTH_OKTA_DOMAIN} + authServerId: ${AUTH_OKTA_AUTH_SERVER_ID} # Optional + idp: ${AUTH_OKTA_IDP} # Optional ``` The values referenced are found on the Application page on your Okta site. @@ -48,8 +50,10 @@ The values referenced are found on the Application page on your Okta site. - `clientId`: The client ID that you generated on Okta, e.g. `3abe134ejxzF21HU74c1` - `clientSecret`: The client secret shown for the Application. -- `audience`: The Okta domain shown for your Application, e.g. +- `audience`: The Okta domain shown for the Application, e.g. `https://company.okta.com` +- `authServerId`: The authorization server ID for the Application +- `idp`: The identity provider for the application, e.g. `0oaulob4BFVa4zQvt0g3` ## Adding the provider to the Backstage frontend diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index b78021c8f1..633584bdb5 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -57,10 +57,17 @@ This is an array used to determine where to retrieve cluster configuration from. Valid cluster locator methods are: +- [`localKubectlProxy`](#localKubectlProxy) - [`config`](#config) - [`gke`](#gke) - [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier) +#### `localKubectlProxy` + +This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001). + +NOTE: This cluster locator method is for local development only and should not be used in production. + #### `config` This cluster locator method will read cluster information from your app-config diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d6b027e6ea..b1c2f39e27 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -40,6 +40,7 @@ "@backstage/config": "^1.0.1", "@backstage/errors": "^1.1.0-next.0", "@backstage/types": "^1.0.0", + "@davidzemon/passport-okta-oauth": "^0.0.5", "@google-cloud/firestore": "^5.0.2", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", @@ -68,7 +69,6 @@ "passport-google-oauth20": "^2.0.0", "passport-microsoft": "^1.0.0", "passport-oauth2": "^1.6.1", - "passport-okta-oauth": "^0.0.1", "passport-onelogin-oauth": "^0.0.1", "passport-saml": "^3.1.2", "uuid": "^8.0.0", diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index de9d128641..cb686f17e9 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -26,7 +26,7 @@ import { OAuthRefreshRequest, OAuthResult, } from '../../lib/oauth'; -import { Strategy as OktaStrategy } from 'passport-okta-oauth'; +import { Strategy as OktaStrategy } from '@davidzemon/passport-okta-oauth'; import passport from 'passport'; import { executeFrameHandlerStrategy, @@ -55,6 +55,8 @@ type PrivateInfo = { export type OktaAuthProviderOptions = OAuthProviderOptions & { audience: string; + authServerId?: string; + idp?: string; signInResolver?: SignInResolver; authHandler: AuthHandler; resolverContext: AuthResolverContext; @@ -94,6 +96,8 @@ export class OktaAuthProvider implements OAuthHandlers { clientSecret: options.clientSecret, callbackURL: options.callbackUrl, audience: options.audience, + authServerID: options.authServerId, + idp: options.idp, passReqToCallback: false, store: this.store, response_type: 'code', @@ -220,6 +224,8 @@ export const okta = createAuthProviderIntegration({ const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const audience = envConfig.getString('audience'); + const authServerId = envConfig.getOptionalString('authServerId'); + const idp = envConfig.getOptionalString('idp'); const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); const callbackUrl = customCallbackUrl || @@ -240,6 +246,8 @@ export const okta = createAuthProviderIntegration({ const provider = new OktaAuthProvider({ audience, + authServerId, + idp, clientId, clientSecret, callbackUrl, diff --git a/plugins/auth-backend/src/providers/okta/types.d.ts b/plugins/auth-backend/src/providers/okta/types.d.ts deleted file mode 100644 index ab83bf89ec..0000000000 --- a/plugins/auth-backend/src/providers/okta/types.d.ts +++ /dev/null @@ -1,20 +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. - */ -declare module 'passport-okta-oauth' { - export class Strategy { - constructor(options: any, verify: any); - } -} diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index cb8a5abb8a..09ca4d8dfe 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -268,6 +268,12 @@ export type CatalogProcessorParser = (options: { location: LocationSpec; }) => AsyncIterable; +// @public (undocumented) +export type CatalogProcessorRefreshKeysResult = { + type: 'refresh'; + key: string; +}; + // @public (undocumented) export type CatalogProcessorRelationResult = { type: 'relation'; @@ -279,7 +285,8 @@ export type CatalogProcessorResult = | CatalogProcessorLocationResult | CatalogProcessorEntityResult | CatalogProcessorRelationResult - | CatalogProcessorErrorResult; + | CatalogProcessorErrorResult + | CatalogProcessorRefreshKeysResult; // @public (undocumented) export class CodeOwnersProcessor implements CatalogProcessor { @@ -545,7 +552,11 @@ export class PlaceholderProcessor implements CatalogProcessor { // (undocumented) getProcessorName(): string; // (undocumented) - preProcessEntity(entity: Entity, location: LocationSpec): Promise; + preProcessEntity( + entity: Entity, + location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise; } // @public (undocumented) @@ -567,6 +578,7 @@ export type PlaceholderResolverParams = { baseUrl: string; read: PlaceholderResolverRead; resolveUrl: PlaceholderResolverResolveUrl; + emit: CatalogProcessorEmit; }; // @public (undocumented) @@ -601,6 +613,7 @@ export const processingResult: Readonly<{ newEntity: Entity, ) => CatalogProcessorResult; readonly relation: (spec: EntityRelationSpec) => CatalogProcessorResult; + readonly refresh: (key: string) => CatalogProcessorResult; }>; // @public (undocumented) diff --git a/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js b/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js new file mode 100644 index 0000000000..b1b67f68f2 --- /dev/null +++ b/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js @@ -0,0 +1,53 @@ +/* + * Copyright 2022 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. + */ + +/** + * @param { import("knex").Knex } knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('refresh_keys', table => { + table.comment( + 'This table contains relations between entities and keys to trigger refreshes with', + ); + table + .text('entity_id') + .notNullable() + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE') + .comment('A reference to the entity that the refresh key is tied to'); + table + .text('key') + .notNullable() + .comment( + 'A reference to a key which should be used to trigger a refresh on this entity', + ); + table.index('entity_id', 'refresh_keys_entity_id_idx'); + table.index('key', 'refresh_keys_key_idx'); + }); +}; + +/** + * @param { import("knex").Knex } knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('refresh_keys', table => { + table.dropIndex([], 'refresh_keys_entity_id_idx'); + table.dropIndex([], 'refresh_keys_key_idx'); + }); + + await knex.schema.dropTable('refresh_keys'); +}; diff --git a/plugins/catalog-backend/src/api/index.ts b/plugins/catalog-backend/src/api/index.ts index 1cb4b71aa3..136c23d20d 100644 --- a/plugins/catalog-backend/src/api/index.ts +++ b/plugins/catalog-backend/src/api/index.ts @@ -26,6 +26,7 @@ export type { CatalogProcessorRelationResult, CatalogProcessorErrorResult, CatalogProcessorResult, + CatalogProcessorRefreshKeysResult, } from './processor'; export type { EntityProvider, diff --git a/plugins/catalog-backend/src/api/processingResult.ts b/plugins/catalog-backend/src/api/processingResult.ts index 2fc2c9eac7..84f1f4b70d 100644 --- a/plugins/catalog-backend/src/api/processingResult.ts +++ b/plugins/catalog-backend/src/api/processingResult.ts @@ -65,4 +65,8 @@ export const processingResult = Object.freeze({ relation(spec: EntityRelationSpec): CatalogProcessorResult { return { type: 'relation', relation: spec }; }, + + refresh(key: string): CatalogProcessorResult { + return { type: 'refresh', key }; + }, } as const); diff --git a/plugins/catalog-backend/src/api/processor.ts b/plugins/catalog-backend/src/api/processor.ts index 3f274d13ea..44e8b298b0 100644 --- a/plugins/catalog-backend/src/api/processor.ts +++ b/plugins/catalog-backend/src/api/processor.ts @@ -169,9 +169,16 @@ export type CatalogProcessorErrorResult = { location: LocationSpec; }; +/** @public */ +export type CatalogProcessorRefreshKeysResult = { + type: 'refresh'; + key: string; +}; + /** @public */ export type CatalogProcessorResult = | CatalogProcessorLocationResult | CatalogProcessorEntityResult | CatalogProcessorRelationResult - | CatalogProcessorErrorResult; + | CatalogProcessorErrorResult + | CatalogProcessorRefreshKeysResult; diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts index 251388f0d2..6a0986b345 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts @@ -24,6 +24,7 @@ import { DateTime } from 'luxon'; import { applyDatabaseMigrations } from './migrations'; import { DefaultProcessingDatabase } from './DefaultProcessingDatabase'; import { + DbRefreshKeysRow, DbRefreshStateReferencesRow, DbRefreshStateRow, DbRelationsRow, @@ -98,6 +99,7 @@ describe('Default Processing Database', () => { resultHash: '', relations: [], deferredEntities: [], + refreshKeys: [], }), ).rejects.toThrow( `Conflicting write of processing result for ${id} with location key 'undefined'`, @@ -117,6 +119,7 @@ describe('Default Processing Database', () => { relations: [], deferredEntities: [], locationKey: 'key', + refreshKeys: [], errors: "['something broke']", }; const { knex, db } = await createDatabase(databaseId); @@ -143,6 +146,7 @@ describe('Default Processing Database', () => { ...options, resultHash: '', locationKey: 'fail', + refreshKeys: [], }), ).rejects.toThrow( `Conflicting write of processing result for ${id} with location key 'fail'`, @@ -174,6 +178,7 @@ describe('Default Processing Database', () => { relations: [], deferredEntities: [], locationKey: 'key', + refreshKeys: [], errors: "['something broke']", }), ); @@ -228,6 +233,7 @@ describe('Default Processing Database', () => { resultHash: '', relations: relations, deferredEntities: [], + refreshKeys: [], }), ); @@ -250,6 +256,7 @@ describe('Default Processing Database', () => { resultHash: '', relations: relations, deferredEntities: [], + refreshKeys: [], }), ); @@ -309,6 +316,7 @@ describe('Default Processing Database', () => { resultHash: '', relations: [], deferredEntities, + refreshKeys: [], }), ); @@ -400,6 +408,7 @@ describe('Default Processing Database', () => { processedEntity, resultHash: '', relations: [], + refreshKeys: [], deferredEntities: [ { entity: { @@ -465,6 +474,63 @@ describe('Default Processing Database', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'stores the refresh keys for the entity', + async databaseId => { + const mockLogger = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }; + const { knex, db } = await createDatabase( + databaseId, + mockLogger as unknown as Logger, + ); + await insertRefreshStateRow(knex, { + entity_id: id, + entity_ref: 'location:default/fakelocation', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: '2021-04-01 13:37:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + + const deferredEntities = [ + { + entity: { + apiVersion: '1', + kind: 'Location', + metadata: { + name: 'next', + }, + }, + locationKey: 'mock', + }, + ]; + + await db.transaction(tx => + db.updateProcessedEntity(tx, { + id, + processedEntity, + resultHash: '', + relations: [], + deferredEntities, + refreshKeys: [{ key: 'protocol:foo-bar.com' }], + }), + ); + + const refreshKeys = await knex('refresh_keys') + .where({ entity_id: id }) + .select(); + + expect(refreshKeys[0]).toEqual({ + entity_id: id, + key: 'protocol:foo-bar.com', + }); + }, + ); }); describe('updateEntityCache', () => { diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index dd28cf1290..903dd59e9e 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -33,12 +33,14 @@ import { UpdateEntityCacheOptions, ListParentsOptions, ListParentsResult, + RefreshByKeyOptions, } from './types'; import { DeferredEntity } from '../processing/types'; import { ProcessingIntervalFunction } from '../processing/refresh'; import { rethrowError, timestampToDateTime } from './conversion'; import { initDatabaseMetrics } from './metrics'; import { + DbRefreshKeysRow, DbRefreshStateReferencesRow, DbRefreshStateRow, DbRelationsRow, @@ -77,6 +79,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { errors, relations, deferredEntities, + refreshKeys, locationKey, } = options; const refreshResult = await tx('refresh_state') @@ -100,11 +103,12 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { `Conflicting write of processing result for ${id} with location key '${locationKey}'`, ); } + const sourceEntityRef = stringifyEntityRef(processedEntity); // Schedule all deferred entities for future processing. await this.addUnprocessedEntities(tx, { entities: deferredEntities, - sourceEntityRef: stringifyEntityRef(processedEntity), + sourceEntityRef, }); // Delete old relations @@ -138,6 +142,21 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { BATCH_SIZE, ); + // Delete old refresh keys + await tx('refresh_keys') + .where({ entity_id: id }) + .delete(); + + // Insert the refresh keys for the processed entity + await tx.batchInsert( + 'refresh_keys', + refreshKeys.map(k => ({ + entity_id: id, + key: k.key, + })), + BATCH_SIZE, + ); + return { previous: { relations: previousRelationRows, @@ -516,6 +535,25 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { } } + async refreshByRefreshKeys( + txOpaque: Transaction, + options: RefreshByKeyOptions, + ) { + const tx = txOpaque as Knex.Transaction; + const { keys } = options; + + await tx('refresh_state') + .whereIn('entity_id', function selectEntityRefs(tx2) { + tx2 + .whereIn('key', keys) + .select({ + entity_id: 'refresh_keys.entity_id', + }) + .from('refresh_keys'); + }) + .update({ next_update_at: tx.fn.now() }); + } + async transaction(fn: (tx: Transaction) => Promise): Promise { try { let result: T | undefined = undefined; diff --git a/plugins/catalog-backend/src/database/tables.ts b/plugins/catalog-backend/src/database/tables.ts index 40b4cb9a12..b9fe12be11 100644 --- a/plugins/catalog-backend/src/database/tables.ts +++ b/plugins/catalog-backend/src/database/tables.ts @@ -43,6 +43,11 @@ export type DbRefreshStateRow = { location_key?: string; }; +export type DbRefreshKeysRow = { + entity_id: string; + key: string; +}; + export type DbRefreshStateReferencesRow = { source_key?: string; source_entity_ref?: string; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index ce45a88938..8a839c0650 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { DateTime } from 'luxon'; import { EntityRelationSpec } from '../api'; -import { DeferredEntity } from '../processing/types'; +import { DeferredEntity, RefreshKeyData } from '../processing/types'; import { DbRelationsRow } from './tables'; /** @@ -38,6 +38,7 @@ export type UpdateProcessedEntityOptions = { relations: EntityRelationSpec[]; deferredEntities: DeferredEntity[]; locationKey?: string; + refreshKeys: RefreshKeyData[]; }; export type UpdateEntityCacheOptions = { @@ -81,6 +82,10 @@ export type ReplaceUnprocessedEntitiesOptions = type: 'delta'; }; +export type RefreshByKeyOptions = { + keys: string[]; +}; + export type RefreshOptions = { entityRef: string; }; diff --git a/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts index a3eb5e4542..0c114f49a1 100644 --- a/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts @@ -43,7 +43,10 @@ describe('FileReaderProcessor', () => { expect(generated.type).toBe('entity'); expect(generated.location).toEqual(spec); - expect(generated.entity).toEqual({ kind: 'Component' }); + expect(generated.entity).toEqual({ + kind: 'Component', + metadata: { name: 'component-test' }, + }); }); it('should fail load from file with error', async () => { @@ -77,14 +80,24 @@ describe('FileReaderProcessor', () => { defaultEntityDataParser, ); - expect(emit).toBeCalledTimes(2); - expect(emit.mock.calls[0][0].entity).toEqual({ kind: 'Component' }); + expect(emit).toBeCalledTimes(4); + expect(emit.mock.calls[0][0].entity).toEqual({ + kind: 'Component', + metadata: { name: 'component-test' }, + }); expect(emit.mock.calls[0][0].location).toEqual({ type: 'file', target: expect.stringMatching(/^[^*]*$/), }); - expect(emit.mock.calls[1][0].entity).toEqual({ kind: 'API' }); - expect(emit.mock.calls[1][0].location).toEqual({ + expect(emit.mock.calls[1][0].key).toContain('file:'); + expect(emit.mock.calls[1][0].key).toContain( + 'fileReaderProcessor/component.yaml', + ); + expect(emit.mock.calls[2][0].entity).toEqual({ + kind: 'API', + metadata: { name: 'api-test' }, + }); + expect(emit.mock.calls[2][0].location).toEqual({ type: 'file', target: expect.stringMatching(/^[^*]*$/), }); diff --git a/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts index 66c6479fa7..8ececed59f 100644 --- a/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts @@ -28,6 +28,8 @@ import { const glob = promisify(g); +const LOCATION_TYPE = 'file'; + /** @public */ export class FileReaderProcessor implements CatalogProcessor { getProcessorName(): string { @@ -40,7 +42,7 @@ export class FileReaderProcessor implements CatalogProcessor { emit: CatalogProcessorEmit, parser: CatalogProcessorParser, ): Promise { - if (location.type !== 'file') { + if (location.type !== LOCATION_TYPE) { return false; } @@ -50,17 +52,23 @@ export class FileReaderProcessor implements CatalogProcessor { if (fileMatches.length > 0) { for (const fileMatch of fileMatches) { const data = await fs.readFile(fileMatch); + const normalizedFilePath = path.normalize(fileMatch); // The normalize converts to native slashes; the glob library returns // forward slashes even on windows for await (const parseResult of parser({ data: data, location: { - type: 'file', - target: path.normalize(fileMatch), + type: LOCATION_TYPE, + target: normalizedFilePath, }, })) { emit(parseResult); + emit( + processingResult.refresh( + `${LOCATION_TYPE}:${normalizedFilePath}`, + ), + ); } } } else if (!optional) { diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index adff97336e..9bfa9027d9 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -17,6 +17,7 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { CatalogProcessorResult } from '../../api'; import { jsonPlaceholderResolver, PlaceholderProcessor, @@ -51,7 +52,7 @@ describe('PlaceholderProcessor', () => { integrations, }); await expect( - processor.preProcessEntity(input, { type: 't', target: 'l' }), + processor.preProcessEntity(input, { type: 't', target: 'l' }, () => {}), ).resolves.toBe(input); }); @@ -76,6 +77,7 @@ describe('PlaceholderProcessor', () => { spec: { a: [{ b: { $upper: 'text' } }] }, }, { type: 'fake', target: 'http://example.com' }, + () => {}, ), ).resolves.toEqual({ apiVersion: 'a', @@ -110,7 +112,7 @@ describe('PlaceholderProcessor', () => { }; await expect( - processor.preProcessEntity(entity, { type: 'a', target: 'b' }), + processor.preProcessEntity(entity, { type: 'a', target: 'b' }, () => {}), ).resolves.toEqual(entity); expect(read).not.toBeCalled(); @@ -131,7 +133,7 @@ describe('PlaceholderProcessor', () => { }; await expect( - processor.preProcessEntity(entity, { type: 'a', target: 'b' }), + processor.preProcessEntity(entity, { type: 'a', target: 'b' }, () => {}), ).resolves.toEqual(entity); expect(read).not.toBeCalled(); @@ -158,6 +160,7 @@ describe('PlaceholderProcessor', () => { target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, + () => {}, ), ).resolves.toEqual({ apiVersion: 'a', @@ -194,6 +197,7 @@ describe('PlaceholderProcessor', () => { target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, + () => {}, ), ).resolves.toEqual({ apiVersion: 'a', @@ -228,6 +232,7 @@ describe('PlaceholderProcessor', () => { target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, + () => {}, ), ).resolves.toEqual({ apiVersion: 'a', @@ -266,6 +271,7 @@ describe('PlaceholderProcessor', () => { target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, + () => {}, ), ).resolves.toEqual({ apiVersion: 'a', @@ -303,6 +309,7 @@ describe('PlaceholderProcessor', () => { type: 'url', target: './a/b/catalog-info.yaml', }, + () => {}, ), ).resolves.toEqual({ apiVersion: 'a', @@ -343,6 +350,7 @@ describe('PlaceholderProcessor', () => { type: 'url', target: './a/b/catalog-info.yaml', }, + () => {}, ), ).rejects.toThrow( /^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError \[ERR_INVALID_URL\]/, @@ -350,6 +358,35 @@ describe('PlaceholderProcessor', () => { expect(read).not.toBeCalled(); }); + it('should emit the resolverValue as a refreshKey', async () => { + read.mockResolvedValue( + Buffer.from(JSON.stringify({ a: ['b', 7] }), 'utf-8'), + ); + + const processor = new PlaceholderProcessor({ + resolvers: { + json: jsonPlaceholderResolver, + }, + reader, + integrations, + }); + + const emitted = new Array(); + await processor.preProcessEntity( + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'n' }, + spec: { a: [{ b: { $json: './path-to-file.json' } }] }, + }, + { type: 'fake', target: 'http://example.com' }, + result => emitted.push(result), + ); + expect(emitted[0]).toEqual({ + type: 'refresh', + key: 'url:http://example.com/path-to-file.json', + }); + }); }); describe('yamlPlaceholderResolver', () => { @@ -360,6 +397,7 @@ describe('yamlPlaceholderResolver', () => { baseUrl: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', read, resolveUrl: (url, base) => integrations.resolveUrl({ url, base }), + emit: () => {}, }; beforeEach(() => { @@ -405,6 +443,7 @@ describe('jsonPlaceholderResolver', () => { baseUrl: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', read, resolveUrl: (url, base) => integrations.resolveUrl({ url, base }), + emit: () => {}, }; beforeEach(() => { diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index ddd5f951db..be2e9f9f5b 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -19,7 +19,12 @@ import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import yaml from 'yaml'; -import { CatalogProcessor, LocationSpec } from '../../api'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; /** @public */ export type PlaceholderResolverRead = (url: string) => Promise; @@ -37,6 +42,7 @@ export type PlaceholderResolverParams = { baseUrl: string; read: PlaceholderResolverRead; resolveUrl: PlaceholderResolverResolveUrl; + emit: CatalogProcessorEmit; }; /** @public */ @@ -66,6 +72,7 @@ export class PlaceholderProcessor implements CatalogProcessor { async preProcessEntity( entity: Entity, location: LocationSpec, + emit: CatalogProcessorEmit, ): Promise { const process = async (data: any): Promise<[any, boolean]> => { if (!data || !(data instanceof Object)) { @@ -102,6 +109,7 @@ export class PlaceholderProcessor implements CatalogProcessor { const resolverKey = keys[0].substr(1); const resolverValue = data[keys[0]]; + const resolver = this.options.resolvers[resolverKey]; if (!resolver || typeof resolverValue !== 'string') { // If there was no such placeholder resolver or if the value was not a @@ -134,6 +142,7 @@ export class PlaceholderProcessor implements CatalogProcessor { baseUrl: location.target, read, resolveUrl, + emit, }), true, ]; @@ -151,11 +160,13 @@ export class PlaceholderProcessor implements CatalogProcessor { export async function yamlPlaceholderResolver( params: PlaceholderResolverParams, ): Promise { - const text = await readTextLocation(params); + const { content, url } = await readTextLocation(params); + + params.emit(processingResult.refresh(`url:${url}`)); let documents: yaml.Document.Parsed[]; try { - documents = yaml.parseAllDocuments(text).filter(d => d); + documents = yaml.parseAllDocuments(content).filter(d => d); } catch (e) { throw new Error( `Placeholder \$${params.key} failed to parse YAML data at ${params.value}, ${e}`, @@ -182,10 +193,12 @@ export async function yamlPlaceholderResolver( export async function jsonPlaceholderResolver( params: PlaceholderResolverParams, ): Promise { - const text = await readTextLocation(params); + const { content, url } = await readTextLocation(params); + + params.emit(processingResult.refresh(`url:${url}`)); try { - return JSON.parse(text); + return JSON.parse(content); } catch (e) { throw new Error( `Placeholder \$${params.key} failed to parse JSON data at ${params.value}, ${e}`, @@ -196,7 +209,11 @@ export async function jsonPlaceholderResolver( export async function textPlaceholderResolver( params: PlaceholderResolverParams, ): Promise { - return await readTextLocation(params); + const { content, url } = await readTextLocation(params); + + params.emit(processingResult.refresh(`url:${url}`)); + + return content; } /* @@ -205,12 +222,12 @@ export async function textPlaceholderResolver( async function readTextLocation( params: PlaceholderResolverParams, -): Promise { +): Promise<{ content: string; url: string }> { const newUrl = relativeUrl(params); try { const data = await params.read(newUrl); - return data.toString('utf-8'); + return { content: data.toString('utf-8'), url: newUrl }; } catch (e) { throw new Error( `Placeholder \$${params.key} could not read location ${params.value}, ${e}`, diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index dbcb9e34df..fb3f48c939 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -61,7 +61,13 @@ describe('UrlReaderProcessor', () => { server.use( rest.get(`${mockApiOrigin}/component.yaml`, (_, res, ctx) => - res(ctx.set({ ETag: 'my-etag' }), ctx.json({ mock: 'entity' })), + res( + ctx.set({ ETag: 'my-etag' }), + ctx.json({ + kind: 'component', + metadata: { name: 'mock-url-entity' }, + }), + ), ), ); @@ -74,15 +80,25 @@ describe('UrlReaderProcessor', () => { mockCache, ); - expect(emitted.length).toBe(1); + expect(emitted.length).toBe(2); expect(emitted[0]).toEqual({ type: 'entity', location: spec, - entity: { mock: 'entity' }, + entity: { kind: 'component', metadata: { name: 'mock-url-entity' } }, + }); + expect(emitted[1]).toEqual({ + type: 'refresh', + key: 'url:http://localhost/component.yaml', }); expect(mockCache.set).toBeCalledWith('v1', { etag: 'my-etag', - value: [{ type: 'entity', location: spec, entity: { mock: 'entity' } }], + value: [ + { + type: 'entity', + location: spec, + entity: { kind: 'component', metadata: { name: 'mock-url-entity' } }, + }, + ], }); expect(mockCache.set).toBeCalledTimes(1); }); diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index 7f27bcd19d..7b62690341 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -93,6 +93,8 @@ export class UrlReaderProcessor implements CatalogProcessor { value: parseResults as CatalogProcessorEntityResult[], }); } + + emit(processingResult.refresh(`${location.type}:${location.target}`)); } catch (error) { assertError(error); const message = `Unable to read ${location.type}, ${error}`; diff --git a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml index 8524aaf14a..b5844bd6ed 100644 --- a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml +++ b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml @@ -1 +1,3 @@ kind: Component +metadata: + name: component-test diff --git a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml index a894e34f70..ad33548f13 100644 --- a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml +++ b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml @@ -1 +1,3 @@ kind: API +metadata: + name: api-test diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts index 10de36a1be..3b6f126391 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts @@ -30,6 +30,7 @@ describe('DefaultCatalogProcessingEngine', () => { updateProcessedEntity: jest.fn(), updateEntityCache: jest.fn(), listParents: jest.fn(), + setRefreshKeys: jest.fn(), } as unknown as jest.Mocked; const orchestrator: jest.Mocked = { process: jest.fn(), @@ -58,6 +59,7 @@ describe('DefaultCatalogProcessingEngine', () => { errors: [], deferredEntities: [], state: {}, + refreshKeys: [], }); const engine = new DefaultCatalogProcessingEngine( getVoidLogger(), @@ -123,6 +125,7 @@ describe('DefaultCatalogProcessingEngine', () => { errors: [], deferredEntities: [], state: {}, + refreshKeys: [], }); const engine = new DefaultCatalogProcessingEngine( getVoidLogger(), @@ -203,6 +206,7 @@ describe('DefaultCatalogProcessingEngine', () => { errors: [], deferredEntities: [], state: {}, + refreshKeys: [], }); const engine = new DefaultCatalogProcessingEngine( @@ -413,6 +417,7 @@ describe('DefaultCatalogProcessingEngine', () => { errors: [], deferredEntities: [], state: {}, + refreshKeys: [], }) .mockResolvedValueOnce({ ok: true, @@ -432,6 +437,7 @@ describe('DefaultCatalogProcessingEngine', () => { errors: [], deferredEntities: [], state: {}, + refreshKeys: [], }); await engine.start(); diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index b7ef5c9344..82062a8e2c 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -134,6 +134,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { .update(stableStringify({ ...result.completedEntity })) .update(stableStringify([...result.deferredEntities])) .update(stableStringify([...result.relations])) + .update(stableStringify([...result.refreshKeys])) .update(stableStringify([...parents])); } @@ -180,6 +181,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { relations: result.relations, deferredEntities: result.deferredEntities, locationKey, + refreshKeys: result.refreshKeys, }); oldRelationSources = new Set( previous.relations.map(r => r.source_entity_ref), diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index 6e494d784e..b2d9846391 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -102,6 +102,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { ok: true, completedEntity: entity, deferredEntities: [], + refreshKeys: [], errors: [], relations: [], state: { @@ -119,6 +120,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { ).resolves.toEqual({ ok: true, completedEntity: entity, + refreshKeys: [], deferredEntities: [ { locationKey: 'url:./new-place', diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index db7a050dce..033dfd4ae3 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -24,7 +24,7 @@ import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; import { CatalogProcessorResult, EntityRelationSpec } from '../api'; import { locationSpecToLocationEntity } from '../util/conversion'; -import { DeferredEntity } from './types'; +import { DeferredEntity, RefreshKeyData } from './types'; import { getEntityLocationRef, getEntityOriginLocationRef, @@ -38,6 +38,7 @@ export class ProcessorOutputCollector { private readonly errors = new Array(); private readonly relations = new Array(); private readonly deferredEntities = new Array(); + private readonly refreshKeys = new Array(); private done = false; constructor( @@ -54,6 +55,7 @@ export class ProcessorOutputCollector { return { errors: this.errors, relations: this.relations, + refreshKeys: this.refreshKeys, deferredEntities: this.deferredEntities, }; } @@ -116,6 +118,8 @@ export class ProcessorOutputCollector { this.relations.push(i.relation); } else if (i.type === 'error') { this.errors.push(i.error); + } else if (i.type === 'refresh') { + this.refreshKeys.push({ key: i.key }); } } } diff --git a/plugins/catalog-backend/src/processing/types.ts b/plugins/catalog-backend/src/processing/types.ts index b178522c14..e94125b6f5 100644 --- a/plugins/catalog-backend/src/processing/types.ts +++ b/plugins/catalog-backend/src/processing/types.ts @@ -26,7 +26,6 @@ export type EntityProcessingRequest = { entity: Entity; state?: JsonObject; // Versions for multiple deployments etc }; - /** * The result of processing an entity. * @public @@ -38,6 +37,7 @@ export type EntityProcessingResult = completedEntity: Entity; deferredEntities: DeferredEntity[]; relations: EntityRelationSpec[]; + refreshKeys: RefreshKeyData[]; errors: Error[]; } | { @@ -45,6 +45,14 @@ export type EntityProcessingResult = errors: Error[]; }; +/** + * A string to associate to the entity itself. + * @public + */ +export type RefreshKeyData = { + key: string; +}; + /** * Responsible for executing the individual processing steps in order to fully process an entity. * @public diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index bd166cbd50..8151bbf18d 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts @@ -48,6 +48,7 @@ describe('DefaultLocationServiceTest', () => { name: 'foo', }, }, + refreshKeys: [], deferredEntities: [ { entity: { @@ -75,6 +76,7 @@ describe('DefaultLocationServiceTest', () => { }, }, deferredEntities: [], + refreshKeys: [], relations: [], errors: [], }); @@ -134,6 +136,7 @@ describe('DefaultLocationServiceTest', () => { }, }, deferredEntities: [], + refreshKeys: [], relations: [], errors: [], }); @@ -161,6 +164,7 @@ describe('DefaultLocationServiceTest', () => { name: 'foo', }, }, + refreshKeys: [], deferredEntities: [ { entity: { @@ -188,6 +192,7 @@ describe('DefaultLocationServiceTest', () => { }, }, deferredEntities: [], + refreshKeys: [], relations: [], errors: [], }); @@ -211,6 +216,7 @@ describe('DefaultLocationServiceTest', () => { name: 'bar', }, }, + refreshKeys: [], deferredEntities: [], relations: [], errors: [], diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index 84bfde1259..419aedc15d 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -138,6 +138,7 @@ describe('Refresh integration', () => { errors: [], deferredEntities, state: {}, + refreshKeys: [], }; }, }, diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 8eafd8bfa3..16a36eb8c4 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -409,6 +409,7 @@ describe('createRouter readonly disabled', () => { state: {}, completedEntity: entity, deferredEntities: [], + refreshKeys: [], relations: [], errors: [], }); diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md index f7bcdd3973..93dd181c10 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/README.md +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -2,48 +2,34 @@ This can be used to run the kubernetes plugin locally against a mock service. -# Viewing in local Minikube running Backstage locally +# Viewing in local Kind running Backstage locally ## Prerequisites -- kubectl installed -- Minikube installed, with the following addons - - metrics-server - - ingress -- jq installed +- [kubectl installed](https://kubernetes.io/docs/tasks/tools/#kubectl) +- [Kind installed](https://kind.sigs.k8s.io/docs/user/quick-start/) - Backstage locally built and ready to run ## Steps -1. Start minikube -2. Get the Kubernetes master base URL `kubectl cluster-info` -3. Apply manifests `kubectl apply -f dice-roller-manifests.yaml` -4. Get service account token (see below) -5. Start Backstage UI and backend -6. Register existing component in Backstage - - https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml +1. Start kind +2. Apply manifests `kubectl apply -f plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml` +3. Run `kubectl proxy` +4. In separate terminal windows start Backstage UI and backend +5. Register a test component ([example](https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml)) +6. Visit [kubernetes plugin page](http://localhost:3000/catalog/default/component/dice-roller/kubernetes) -Add or update `app-config.local.yaml` with the following: +### Example `app-config.local.yaml` ```yaml kubernetes: serviceLocatorMethod: type: 'multiTenant' clusterLocatorMethods: - - type: 'config' - clusters: - - url: - name: minikube - serviceAccountToken: - authProvider: 'serviceAccount' + - type: 'localKubectlProxy' + +catalog: + locations: + - type: url + target: https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml ``` - -### Getting the service account token - -Mac copy to clipboard: - -``` -kubectl get secret $(kubectl get sa dice-roller -o=json | jq -r '.secrets[0].name') -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy -``` - -Paste into `app-config.local.yaml` `kubernetes.clusters[0].serviceAccountToken` diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleServiceAccountAuthProvider.ts b/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts similarity index 54% rename from plugins/kubernetes/src/kubernetes-auth-provider/GoogleServiceAccountAuthProvider.ts rename to plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts index 9f522bea64..b2c11fc744 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleServiceAccountAuthProvider.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts @@ -14,16 +14,25 @@ * limitations under the License. */ -import { KubernetesAuthProvider } from './types'; -import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; -export class GoogleServiceAccountAuthProvider - implements KubernetesAuthProvider +export class LocalKubectlProxyClusterLocator + implements KubernetesClustersSupplier { - async decorateRequestBodyForAuth( - requestBody: KubernetesRequestBody, - ): Promise { - // No-op, with google service account auth, server's AWS credentials are used for access - return requestBody; + private readonly clusterDetails: ClusterDetails[]; + + public constructor() { + this.clusterDetails = [ + { + name: 'local', + url: 'http:/localhost:8001', + authProvider: 'localKubectlProxy', + skipMetricsLookup: true, + }, + ]; + } + + async getClusters(): Promise { + return this.clusterDetails; } } diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index 53aeb44f8b..d32f7ffee8 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -19,6 +19,7 @@ import { Duration } from 'luxon'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { ConfigClusterLocator } from './ConfigClusterLocator'; import { GkeClusterLocator } from './GkeClusterLocator'; +import { LocalKubectlProxyClusterLocator } from './LocalKubectlProxyLocator'; class CombinedClustersSupplier implements KubernetesClustersSupplier { constructor(readonly clusterSuppliers: KubernetesClustersSupplier[]) {} @@ -45,6 +46,8 @@ export const getCombinedClusterSupplier = ( .map(clusterLocatorMethod => { const type = clusterLocatorMethod.getString('type'); switch (type) { + case 'localKubectlProxy': + return new LocalKubectlProxyClusterLocator(); case 'config': return ConfigClusterLocator.fromConfig(clusterLocatorMethod); case 'gke': diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts index c2df3e6c23..24de85d7b7 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts @@ -49,6 +49,9 @@ export class KubernetesAuthTranslatorGenerator { case 'oidc': { return new OidcKubernetesAuthTranslator(); } + case 'localKubectlProxy': { + return new NoopKubernetesAuthTranslator(); + } default: { throw new Error( `authProvider "${authProvider}" has no KubernetesAuthTranslator associated with it`, diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index dc2c222712..1a95446e5f 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -32,17 +32,6 @@ import { V1ReplicaSet } from '@kubernetes/client-node'; import { V1Service } from '@kubernetes/client-node'; import { V1StatefulSet } from '@kubernetes/client-node'; -// Warning: (ae-forgotten-export) The symbol "KubernetesAuthProvider" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "AwsKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class AwsKubernetesAuthProvider implements KubernetesAuthProvider { - // (undocumented) - decorateRequestBodyForAuth( - requestBody: KubernetesRequestBody, - ): Promise; -} - // Warning: (ae-forgotten-export) The symbol "ClusterProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Cluster" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -147,6 +136,7 @@ export function formatClusterLink( options: FormatClusterLinkOptions, ): string | undefined; +// Warning: (ae-forgotten-export) The symbol "KubernetesAuthProvider" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "GoogleKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -160,18 +150,6 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { ): Promise; } -// Warning: (ae-missing-release-tag) "GoogleServiceAccountAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class GoogleServiceAccountAuthProvider - implements KubernetesAuthProvider -{ - // (undocumented) - decorateRequestBodyForAuth( - requestBody: KubernetesRequestBody, - ): Promise; -} - // Warning: (ae-missing-release-tag) "GroupedResponses" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -386,10 +364,8 @@ export const PodsTable: ({ // @public (undocumented) export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element; -// Warning: (ae-missing-release-tag) "ServiceAccountKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class ServiceAccountKubernetesAuthProvider +// @public +export class ServerSideKubernetesAuthProvider implements KubernetesAuthProvider { // (undocumented) diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/AwsKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/AwsKubernetesAuthProvider.ts deleted file mode 100644 index 789da9af50..0000000000 --- a/plugins/kubernetes/src/kubernetes-auth-provider/AwsKubernetesAuthProvider.ts +++ /dev/null @@ -1,27 +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. - */ - -import { KubernetesAuthProvider } from './types'; -import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; - -export class AwsKubernetesAuthProvider implements KubernetesAuthProvider { - async decorateRequestBodyForAuth( - requestBody: KubernetesRequestBody, - ): Promise { - // No-op, with aws auth, server's AWS credentials are used for access - return requestBody; - } -} diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/AzureKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/AzureKubernetesAuthProvider.ts deleted file mode 100644 index 60401bbe4d..0000000000 --- a/plugins/kubernetes/src/kubernetes-auth-provider/AzureKubernetesAuthProvider.ts +++ /dev/null @@ -1,27 +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. - */ - -import { KubernetesAuthProvider } from './types'; -import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; - -export class AzureKubernetesAuthProvider implements KubernetesAuthProvider { - async decorateRequestBodyForAuth( - requestBody: KubernetesRequestBody, - ): Promise { - // No-op, with azure auth, server's Azure credentials are used for access - return requestBody; - } -} diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index bcd1b2911a..eb3ea456ea 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -17,11 +17,8 @@ import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types'; import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; -import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider'; -import { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider'; +import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider'; import { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api'; -import { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider'; -import { AzureKubernetesAuthProvider } from './AzureKubernetesAuthProvider'; import { OidcKubernetesAuthProvider } from './OidcKubernetesAuthProvider'; export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { @@ -41,16 +38,23 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { ); this.kubernetesAuthProviderMap.set( 'serviceAccount', - new ServiceAccountKubernetesAuthProvider(), + new ServerSideKubernetesAuthProvider(), ); this.kubernetesAuthProviderMap.set( 'googleServiceAccount', - new GoogleServiceAccountAuthProvider(), + new ServerSideKubernetesAuthProvider(), + ); + this.kubernetesAuthProviderMap.set( + 'aws', + new ServerSideKubernetesAuthProvider(), ); - this.kubernetesAuthProviderMap.set('aws', new AwsKubernetesAuthProvider()); this.kubernetesAuthProviderMap.set( 'azure', - new AzureKubernetesAuthProvider(), + new ServerSideKubernetesAuthProvider(), + ); + this.kubernetesAuthProviderMap.set( + 'localKubectlProxy', + new ServerSideKubernetesAuthProvider(), ); if (options.oidcProviders) { diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts similarity index 76% rename from plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts rename to plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts index 5671112ac8..1c475b9568 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 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. @@ -17,13 +17,18 @@ import { KubernetesAuthProvider } from './types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; -export class ServiceAccountKubernetesAuthProvider +/** + * No-op KubernetesAuthProvider, authorization will be handled in the kubernetes-backend plugin + * + * @public + */ +export class ServerSideKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - // No-op, with service account for auth, cluster config/details should already have serviceAccountToken + // No-op, auth will be taken care of on the server-side return requestBody; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/index.ts b/plugins/kubernetes/src/kubernetes-auth-provider/index.ts index 6f8e3f415f..23197321f6 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/index.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/index.ts @@ -17,7 +17,5 @@ export { kubernetesAuthProvidersApiRef } from './types'; export type { KubernetesAuthProvidersApi } from './types'; export { KubernetesAuthProviders } from './KubernetesAuthProviders'; -export { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider'; export { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; -export { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider'; -export { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider'; +export { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider'; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index a70e60f714..a1bb552435 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -45,7 +45,7 @@ "@backstage/types": "^1.0.0", "@gitbeaker/core": "^35.6.0", "@gitbeaker/node": "^35.1.0", - "@octokit/webhooks": "^9.14.1", + "@octokit/webhooks": "^10.0.0", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "command-exists": "^1.2.9", diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 2bc36ff0d2..60a761d171 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -133,11 +133,13 @@ export const ScaffolderPageContents = ({ export const ScaffolderPage = ({ TemplateCardComponent, groups, + contextMenu, }: ScaffolderPageProps) => ( ); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index b36b332d9e..f0ef5ba4b5 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -89,12 +89,12 @@ export const RepoUrlPicker = ( /* we deal with calling the repo setting here instead of in each components for ease */ useEffect(() => { - if (allowedOwners.length === 1) { + if (allowedOwners.length > 0) { setState(prevState => ({ ...prevState, owner: allowedOwners[0] })); } }, [setState, allowedOwners]); useEffect(() => { - if (allowedRepos.length === 1) { + if (allowedRepos.length > 0) { setState(prevState => ({ ...prevState, repoName: allowedRepos[0] })); } }, [setState, allowedRepos]); diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 44ffa6587f..131635a1de 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -36,6 +36,8 @@ jest.useFakeTimers(); const testFactRetriever: FactRetriever = { id: 'test_factretriever', version: '0.0.1', + title: 'Test 1', + description: 'testing', entityFilter: [{ kind: 'component' }], schema: { testnumberfact: { diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts index caa19dbeaf..3103f9bdd5 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts @@ -30,6 +30,9 @@ import isEmpty from 'lodash/isEmpty'; export const entityMetadataFactRetriever: FactRetriever = { id: 'entityMetadataFactRetriever', version: '0.0.1', + title: 'Entity Metadata', + description: + 'Generates facts which indicate the completeness of entity metadata', schema: { hasTitle: { type: 'boolean', diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts index c1e9b2710e..974f1d30cd 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts @@ -29,6 +29,9 @@ import { Entity } from '@backstage/catalog-model'; export const entityOwnershipFactRetriever: FactRetriever = { id: 'entityOwnershipFactRetriever', version: '0.0.1', + title: 'Entity Ownership', + description: + 'Generates facts which indicate the quality of data in the spec.owner field', entityFilter: [ { kind: ['component', 'domain', 'system', 'api', 'resource', 'template'] }, ], diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts index 6bc84f4aa9..dc14298a55 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts @@ -34,6 +34,9 @@ const techdocsAnnotationFactName = export const techdocsFactRetriever: FactRetriever = { id: 'techdocsFactRetriever', version: '0.0.1', + title: 'Tech Docs', + description: + 'Generates facts related to the completeness of techdocs configuration for entities', schema: { [techdocsAnnotationFactName]: { type: 'boolean', diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 09d409fecd..84677bdcbc 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -46,12 +46,14 @@ export type FactLifecycle = TTL | MaxItems; // @public export interface FactRetriever { + description?: string; entityFilter?: | Record[] | Record; handler: (ctx: FactRetrieverContext) => Promise; id: string; schema: FactSchema; + title?: string; version: string; } diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index bcc2aa42e1..cda238694f 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -174,6 +174,16 @@ export interface FactRetriever { */ version: string; + /** + * A short display title for the fact retriever to be used in the interface + */ + title?: string; + + /** + * A short display description for the fact retriever to be used in the interface. + */ + description?: string; + /** * Handler function that needs to be implemented to retrieve fact values for entities. * diff --git a/yarn.lock b/yarn.lock index f06a187ebf..22f1bcbb81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2126,6 +2126,16 @@ dependencies: "@date-io/core" "^1.3.13" +"@davidzemon/passport-okta-oauth@^0.0.5": + version "0.0.5" + resolved "https://registry.npmjs.org/@davidzemon/passport-okta-oauth/-/passport-okta-oauth-0.0.5.tgz#905d14521f4e8b422f4074c1f7fe1cbe0c88e44b" + integrity sha512-eaC2Ve2MIoqR7dLKgpHxhVKRcfgJCes0Fozxm5SefZh/zqLNb8tGIou+dj0EbylksLmB+nVlhr8p8qwjA9n2sA== + dependencies: + "@types/passport-oauth2" "^1.4.11" + passport-oauth2 "^1.6.1" + pkginfo "^0.4.1" + uid2 "^1.0.0" + "@elastic/elasticsearch-mock@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@elastic/elasticsearch-mock/-/elasticsearch-mock-1.0.0.tgz#1fbaf6bf220cd74a34494ff3a8673551d9dfc66f" @@ -2348,10 +2358,10 @@ teeny-request "^8.0.0" uuid "^8.0.0" -"@graphiql/react@^0.4.3": - version "0.4.3" - resolved "https://registry.npmjs.org/@graphiql/react/-/react-0.4.3.tgz#29ca3125c7a349de5e9beb2aef660137fbcbde2e" - integrity sha512-MnH+7LqRqFnwo8YBDtgDfJOdeS5QqNTzH2gTvTbEYYWaNQW1+Vhbsyu2RKHnPSgKVQlfLzdjw6SAMtSb3q1Qog== +"@graphiql/react@^0.5.0": + version "0.5.0" + resolved "https://registry.npmjs.org/@graphiql/react/-/react-0.5.0.tgz#3bc00de581b09d962a9f391ed725d06f34a757d3" + integrity sha512-Tn0V4yTcRPBgBVq9SVkqGkmlUi7aqmOrn4+vmhwnlxSFJU7mnKak7DZRc9wPVFBTdcHc0pWNKBzY/demFHSxSw== dependencies: "@graphiql/toolkit" "^0.6.0" codemirror "^5.65.3" @@ -5212,7 +5222,22 @@ resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-5.8.0.tgz#b76d1a3e3ad82cec5680d3c6c3443a620047a6ef" integrity sha512-8adktjIb76A7viIdayQSFuBEwOzwhDC+9yxZpKNHjfzrlostHCw0/N7JWpWMObfElwvJMk2fY2l1noENCk9wmw== -"@octokit/webhooks@^9.0.1", "@octokit/webhooks@^9.14.1": +"@octokit/webhooks-types@6.2.0": + version "6.2.0" + resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-6.2.0.tgz#ae84341dadd4a44d032f58b2dce7e3b766ef9da0" + integrity sha512-1YOSIrdb7WwLXHfCwa+7+VKcsZZ0sRsCiqvYvBwzstF4xg0qZ0w2K5Y8OkNvd7NtzKPP7KKgP9hSQZUccbPg0Q== + +"@octokit/webhooks@^10.0.0": + version "10.0.3" + resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-10.0.3.tgz#13c20477b397731bff15e2578f6c86984bb41432" + integrity sha512-ormPrmocaqcZZdGOrLLlVH+FKJJY+I8fdUXzXRhiVpYCBctXHkfJ92MwNDq5vdQ4XeC2IacDhNEu1vfGfwGyFg== + dependencies: + "@octokit/request-error" "^2.0.2" + "@octokit/webhooks-methods" "^2.0.0" + "@octokit/webhooks-types" "6.2.0" + aggregate-error "^3.1.0" + +"@octokit/webhooks@^9.0.1": version "9.26.0" resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.26.0.tgz#cf453bb313da3b66f1a90c84464d978e1c625cce" integrity sha512-foZlsgrTDwAmD5j2Czn6ji10lbWjGDVsUxTIydjG9KTkAWKJrFapXJgO5SbGxRwfPd3OJdhK3nA2YPqVhxLXqA== @@ -6333,9 +6358,9 @@ "@types/range-parser" "*" "@types/express-session@^1.17.2": - version "1.17.4" - resolved "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz#97a30a35e853a61bdd26e727453b8ed314d6166b" - integrity sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg== + version "1.17.5" + resolved "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.5.tgz#13f48852b4aa60ff595835faeb4b4dda0ba0866e" + integrity sha512-l0DhkvNVfyUPEEis8fcwbd46VptfA/jmMwHfob2TfDMf3HyPLiB9mKD71LXhz5TMUobODXPD27zXSwtFQLHm+w== dependencies: "@types/express" "*" @@ -6806,6 +6831,15 @@ "@types/oauth" "*" "@types/passport" "*" +"@types/passport-oauth2@^1.4.11": + version "1.4.11" + resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.11.tgz#fbca527ecb44258774d17bcb251630c321515fa9" + integrity sha512-KUNwmGhe/3xPbjkzkPwwcPmyFwfyiSgtV1qOrPBLaU4i4q9GSCdAOyCbkFG0gUxAyEmYwqo9OAF/rjPjJ6ImdA== + dependencies: + "@types/express" "*" + "@types/oauth" "*" + "@types/passport" "*" + "@types/passport-saml@^1.1.3": version "1.1.3" resolved "https://registry.npmjs.org/@types/passport-saml/-/passport-saml-1.1.3.tgz#efc57902a07ebe1ec114d00acd8d990e873813a1" @@ -8411,9 +8445,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.1122.0, aws-sdk@^2.840.0, aws-sdk@^2.948.0: - version "2.1168.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1168.0.tgz#7936459edee0c999065f1ecf2ef0bea89ea056c1" - integrity sha512-9+WYoYTHHjLqeWdSSLbNpmc/NgnZpX4LGiyYjXenh4WNRBXshXI0XioTK8BQgDscVzB978EJV8kG1nZGE3USzw== + version "2.1169.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1169.0.tgz#06c910092e0dd97ac884ce0939a69c062308184a" + integrity sha512-zBJXLih/iBueYt7hDednt7Jncb+6wzUjDh6JMdl2e0A61xb+Z5o0NNkjbJTUJELdx96u3R4+yil0DoRTOipksA== dependencies: buffer "4.9.2" events "1.1.1" @@ -14174,11 +14208,11 @@ grapheme-splitter@^1.0.4: integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== graphiql@^1.5.12, graphiql@^1.8.8: - version "1.9.10" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.9.10.tgz#ba7113ed8b1bc988d00d8d7cc51901b51b684bde" - integrity sha512-uYYK/zBTV9Y/IHw4uopGVRusbF2xmPnptx7OMFTnTHOTQOmzfIGRTn58WuPgevyEi+9zSLojM86lot9xvNA1/w== + version "1.9.11" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.9.11.tgz#3ade0802a0459792d1533f71e95b39bedce9c3ac" + integrity sha512-/nIFi+y8PNpkC/fb/mmkWUDsX+nIS7DdemxrTPugKpGku/hqVLe0ppXFsJbPuT0UrleL1XASv0sZm+Axh4XGWA== dependencies: - "@graphiql/react" "^0.4.3" + "@graphiql/react" "^0.5.0" "@graphiql/toolkit" "^0.6.0" entities "^2.0.0" graphql-language-service "^5.0.6" @@ -18878,9 +18912,9 @@ modify-values@^1.0.0: integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== moment@^2.27.0, moment@^2.29.1: - version "2.29.3" - resolved "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" - integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== + version "2.29.4" + resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== morgan@^1.10.0: version "1.10.0" @@ -18944,9 +18978,9 @@ msw@^0.39.2: yargs "^17.3.1" msw@^0.43.0: - version "0.43.0" - resolved "https://registry.npmjs.org/msw/-/msw-0.43.0.tgz#10c6fc3fb1752c0a144179e5ab04c6a512cc9959" - integrity sha512-XJylZP0qW3D5WUGWh9FFefJEl3MGG4y1I+/8a833d0eedm6B+GaPm6wPVZNcnlS2YVTagvEgShVJ7ZtY66tTRQ== + version "0.43.1" + resolved "https://registry.npmjs.org/msw/-/msw-0.43.1.tgz#57cb4af56f07442e8a6d14d76032a0ab41434256" + integrity sha512-wzhPpL6RsiYkyIUlTCg0aZY0aRZa4Eiubd6MOA5oJVgfuapDmvZrI8OMi4h4e+fpHD+Qsy+4unAjv3wpWia5yw== dependencies: "@mswjs/cookies" "^0.2.0" "@mswjs/interceptors" "^0.16.3" @@ -20357,15 +20391,6 @@ passport-oauth@1.0.0: passport-oauth1 "1.x.x" passport-oauth2 "1.x.x" -passport-okta-oauth@^0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/passport-okta-oauth/-/passport-okta-oauth-0.0.1.tgz#c8bcee02af3d56ca79d3cca776f2df7cf15a5748" - integrity sha1-yLzuAq89Vsp508yndvLffPFaV0g= - dependencies: - passport-oauth "1.0.0" - pkginfo "0.2.x" - uid2 "0.0.3" - passport-onelogin-oauth@^0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/passport-onelogin-oauth/-/passport-onelogin-oauth-0.0.1.tgz#6e991ac6720783fdd80d4caa08c36cc71ef19962" @@ -20760,7 +20785,7 @@ pkginfo@0.2.x: resolved "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" integrity sha1-cjnEKl72wwuPMoQ52bn/cQQkkPg= -pkginfo@0.4.x: +pkginfo@0.4.x, pkginfo@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= @@ -25269,6 +25294,11 @@ uid2@0.0.3, uid2@0.0.x: resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= +uid2@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz#ef8d95a128d7c5c44defa1a3d052eecc17a06bfb" + integrity sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ== + unbox-primitive@^1.0.1, unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"