From 5211dd8d918cfa63cdd47f559cbba549460a16bb Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Fri, 26 Jan 2024 15:44:30 +0100 Subject: [PATCH 01/46] fix: fix decoding issues in StackOverflowSearchResultListItem Signed-off-by: Tommy Le --- plugins/stack-overflow/package.json | 2 ++ .../StackOverflowSearchResultListItem.tsx | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index de0aec1e61..5364ee59bb 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -54,6 +54,7 @@ "@testing-library/jest-dom": "^6.0.0", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^4.0.0", + "he": "^1.2.0", "lodash": "^4.17.21", "qs": "^6.9.4", "react-use": "^17.2.4" @@ -69,6 +70,7 @@ "@testing-library/dom": "^9.0.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", + "@types/he": "^1.2.3", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx index 245666deeb..9e98d6612f 100644 --- a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx +++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx @@ -15,7 +15,6 @@ */ import React from 'react'; -import _unescape from 'lodash/unescape'; import { Link } from '@backstage/core-components'; import { Divider, @@ -26,8 +25,9 @@ import { Chip, } from '@material-ui/core'; import { useAnalytics } from '@backstage/core-plugin-api'; -import { ResultHighlight } from '@backstage/plugin-search-common'; +import type { ResultHighlight } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; +import { decode } from 'he'; /** * Props for {@link StackOverflowSearchResultListItem} @@ -48,6 +48,10 @@ export const StackOverflowSearchResultListItem = ( const analytics = useAnalytics(); const handleClick = () => { + if (!result) { + return; + } + analytics.captureEvent('discover', result.title, { attributes: { to: result.location }, value: props.rank, @@ -69,12 +73,12 @@ export const StackOverflowSearchResultListItem = ( {highlight?.fields?.title ? ( ) : ( - _unescape(result.title) + decode(result.title) )} } @@ -83,13 +87,13 @@ export const StackOverflowSearchResultListItem = ( <> Author:{' '} ) : ( - `Author: ${result.text}` + `Author: ${decode(result.text)}` ) } /> From c6779aca2da8103128a257afc18e1853f3797e90 Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Fri, 26 Jan 2024 15:49:18 +0100 Subject: [PATCH 02/46] chore: add changeset Signed-off-by: Tommy Le --- .changeset/dry-impalas-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dry-impalas-serve.md diff --git a/.changeset/dry-impalas-serve.md b/.changeset/dry-impalas-serve.md new file mode 100644 index 0000000000..709c0fb498 --- /dev/null +++ b/.changeset/dry-impalas-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow': patch +--- + +fix: fix decode issues in title and author fields in StackOverflowSearchResultListItem From 30fadd61daf864217304fe0a203f15af4d78a3fd Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Tue, 13 Feb 2024 13:25:12 +0100 Subject: [PATCH 03/46] refactor: removed he and added a util to decode html Signed-off-by: Tommy Le --- plugins/stack-overflow/package.json | 2 -- .../StackOverflowSearchResultListItem.tsx | 15 ++++++------- plugins/stack-overflow/src/util.ts | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 plugins/stack-overflow/src/util.ts diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 73498e2a53..c1a4f8f40b 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -54,7 +54,6 @@ "@testing-library/jest-dom": "^6.0.0", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "cross-fetch": "^4.0.0", - "he": "^1.2.0", "lodash": "^4.17.21", "qs": "^6.9.4", "react-use": "^17.2.4" @@ -70,7 +69,6 @@ "@testing-library/dom": "^9.0.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", - "@types/he": "^1.2.3", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx index 9e98d6612f..70e182b86d 100644 --- a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx +++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx @@ -27,7 +27,7 @@ import { import { useAnalytics } from '@backstage/core-plugin-api'; import type { ResultHighlight } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; -import { decode } from 'he'; +import { decodeHtml } from '../../util'; /** * Props for {@link StackOverflowSearchResultListItem} @@ -45,13 +45,10 @@ export const StackOverflowSearchResultListItem = ( props: StackOverflowSearchResultListItemProps, ) => { const { result, highlight } = props; + const analytics = useAnalytics(); const handleClick = () => { - if (!result) { - return; - } - analytics.captureEvent('discover', result.title, { attributes: { to: result.location }, value: props.rank, @@ -73,12 +70,12 @@ export const StackOverflowSearchResultListItem = ( {highlight?.fields?.title ? ( ) : ( - decode(result.title) + decodeHtml(result.title) )} } @@ -87,13 +84,13 @@ export const StackOverflowSearchResultListItem = ( <> Author:{' '} ) : ( - `Author: ${decode(result.text)}` + `Author: ${decodeHtml(result.text)}` ) } /> diff --git a/plugins/stack-overflow/src/util.ts b/plugins/stack-overflow/src/util.ts new file mode 100644 index 0000000000..a40ae905ea --- /dev/null +++ b/plugins/stack-overflow/src/util.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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 function decodeHtml(input: string) { + const textContainer = document.createElement('textarea'); + textContainer.innerHTML = input; + return textContainer.value; +} From 6faf825b662a6e9a9a64d22d9fe55fec81bbe1f8 Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Tue, 13 Feb 2024 13:50:35 +0100 Subject: [PATCH 04/46] chore: update api-report Signed-off-by: Tommy Le --- plugins/stack-overflow/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stack-overflow/api-report.md b/plugins/stack-overflow/api-report.md index 293c2ac226..7593db530f 100644 --- a/plugins/stack-overflow/api-report.md +++ b/plugins/stack-overflow/api-report.md @@ -10,7 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CardExtensionProps } from '@backstage/plugin-home-react'; import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; -import { ResultHighlight } from '@backstage/plugin-search-common'; +import type { ResultHighlight } from '@backstage/plugin-search-common'; import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; // @public From a2327acaab7b36953c2f3cd6204ba19bfc9a3d71 Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Thu, 15 Feb 2024 13:33:56 +0100 Subject: [PATCH 05/46] add word-break: normal to the "moved in direction" table header cell Signed-off-by: Vladimir Kobzev --- .changeset/poor-beans-cross.md | 5 +++++ .../src/components/RadarTimeline/RadarTimeline.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/poor-beans-cross.md diff --git a/.changeset/poor-beans-cross.md b/.changeset/poor-beans-cross.md new file mode 100644 index 0000000000..34c473a545 --- /dev/null +++ b/.changeset/poor-beans-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Fixed an issue with the "moved in direction" table header cell getting squished and becoming unreadable if a timeline description is too long diff --git a/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx index e54c4a499e..2456c26918 100644 --- a/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx +++ b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx @@ -47,7 +47,9 @@ const RadarTimeline = (props: Props): JSX.Element => { - Moved in direction + + Moved in direction + Moved to ring Moved on date Description From f9b9926f4d8a4fc81c70ea8543ec7108cafe8f2b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 22:36:25 +0100 Subject: [PATCH 06/46] docs: add new community plugins project area Signed-off-by: Vincenzo Scamporlino --- OWNERS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/OWNERS.md b/OWNERS.md index 1e92e68c40..b8d317e2d3 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -108,6 +108,20 @@ Scope: The TechDocs plugin and related tooling These incubating project areas have shared ownership with @backstage/maintainers. +### Community Plugins + +Team: @backstage/community-plugins-maintainers + +Scope: Tooling related to the Backstage [Community Plugins repository](https://github.com/backstage/community-plugins) + +| Name | Organization | GitHub | Discord | +| -------------------- | ------------ | ------------------------------------------- | ------------ | +| Bethany Griggs | Red Hat | [BethGriggs](https://github.com/BethGriggs) | `bethgriggs` | +| Tomas Kral | Red Hat | [kadel](https://github.com/kadel) | `tomkral` | +| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| Philipp Hugenroth | Spotify | [tudi2d](https://github.com/tudi2d) | `tudi2d` | +| Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` | + ### OpenAPI Tooling Team: @backstage/openapi-tooling-maintainers From fa7ea3f2f0810ae5b82b6829801208d2599f1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 14:33:10 +0100 Subject: [PATCH 07/46] break out the providers router into a separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/six-sloths-listen.md | 5 + plugins/auth-backend/src/identity/index.ts | 2 +- plugins/auth-backend/src/identity/router.ts | 17 +- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 1 + .../src/{service => providers}/router.test.ts | 0 plugins/auth-backend/src/providers/router.ts | 155 ++++++++++++++++++ plugins/auth-backend/src/service/index.ts | 17 ++ plugins/auth-backend/src/service/router.ts | 133 +++------------ 9 files changed, 208 insertions(+), 124 deletions(-) create mode 100644 .changeset/six-sloths-listen.md rename plugins/auth-backend/src/{service => providers}/router.test.ts (100%) create mode 100644 plugins/auth-backend/src/providers/router.ts create mode 100644 plugins/auth-backend/src/service/index.ts diff --git a/.changeset/six-sloths-listen.md b/.changeset/six-sloths-listen.md new file mode 100644 index 0000000000..47886e3f77 --- /dev/null +++ b/.changeset/six-sloths-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Internal refactor to break out how the router is constructed diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index a5e0dd4b80..0492836723 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { createOidcRouter } from './router'; +export { bindOidcRouter } from './router'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index c9ae9e685a..c3c419e9ea 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -14,18 +14,21 @@ * limitations under the License. */ +import express from 'express'; import Router from 'express-promise-router'; import { TokenIssuer } from './types'; -export type Options = { - baseUrl: string; - tokenIssuer: TokenIssuer; -}; - -export function createOidcRouter(options: Options) { +export function bindOidcRouter( + targetRouter: express.Router, + options: { + baseUrl: string; + tokenIssuer: TokenIssuer; + }, +) { const { baseUrl, tokenIssuer } = options; const router = Router(); + targetRouter.use(router); const config = { issuer: baseUrl, @@ -68,6 +71,4 @@ export function createOidcRouter(options: Options) { router.get('/v1/userinfo', (_req, res) => { res.status(501).send('Not Implemented'); }); - - return router; } diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 3943387e15..6c3f866031 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export { authPlugin as default } from './authPlugin'; -export * from './service/router'; +export * from './service'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 8173a7fbc3..b9543c275c 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -34,6 +34,7 @@ export type { SamlAuthResult } from './saml'; export type { GcpIapResult, GcpIapTokenInfo } from './gcp-iap'; export { providers, defaultAuthProviderFactories } from './providers'; +export { createOriginFilter, type ProviderFactories } from './router'; export { createAuthProviderIntegration } from './createAuthProviderIntegration'; diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/providers/router.test.ts similarity index 100% rename from plugins/auth-backend/src/service/router.test.ts rename to plugins/auth-backend/src/providers/router.test.ts diff --git a/plugins/auth-backend/src/providers/router.ts b/plugins/auth-backend/src/providers/router.ts new file mode 100644 index 0000000000..9971466ad4 --- /dev/null +++ b/plugins/auth-backend/src/providers/router.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2024 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { NotFoundError, assertError } from '@backstage/errors'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Minimatch } from 'minimatch'; +import { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext'; +import { TokenIssuer } from '../identity/types'; + +/** @public */ +export type ProviderFactories = { [s: string]: AuthProviderFactory }; + +export function bindProviderRouters( + targetRouter: express.Router, + options: { + providers: ProviderFactories; + appUrl: string; + baseUrl: string; + config: Config; + logger: LoggerService; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + tokenIssuer: TokenIssuer; + catalogApi?: CatalogApi; + }, +) { + const { + providers, + appUrl, + baseUrl, + config, + logger, + discovery, + tokenManager, + tokenIssuer, + catalogApi, + } = options; + + const providersConfig = config.getOptionalConfig('auth.providers'); + + const isOriginAllowed = createOriginFilter(config); + + for (const [providerId, providerFactory] of Object.entries(providers)) { + if (providersConfig?.has(providerId)) { + logger.info(`Configuring auth provider: ${providerId}`); + try { + const provider = providerFactory({ + providerId, + appUrl, + baseUrl: baseUrl, + isOriginAllowed, + globalConfig: { + baseUrl: baseUrl, + appUrl, + isOriginAllowed, + }, + config: providersConfig.getConfig(providerId), + logger, + resolverContext: CatalogAuthResolverContext.create({ + logger, + catalogApi: + catalogApi ?? new CatalogClient({ discoveryApi: discovery }), + tokenIssuer, + tokenManager, + }), + }); + + const r = Router(); + + r.get('/start', provider.start.bind(provider)); + r.get('/handler/frame', provider.frameHandler.bind(provider)); + r.post('/handler/frame', provider.frameHandler.bind(provider)); + if (provider.logout) { + r.post('/logout', provider.logout.bind(provider)); + } + if (provider.refresh) { + r.get('/refresh', provider.refresh.bind(provider)); + r.post('/refresh', provider.refresh.bind(provider)); + } + + targetRouter.use(`/${providerId}`, r); + } catch (e) { + assertError(e); + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerId} auth provider, ${e.message}`, + ); + } + + logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); + + targetRouter.use(`/${providerId}`, () => { + // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. + throw new NotFoundError( + `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + + `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + + `Check the auth backend plugin logs when the backend starts to see more details.`, + ); + }); + } + } else { + targetRouter.use(`/${providerId}`, () => { + throw new NotFoundError( + `No auth provider registered for '${providerId}'`, + ); + }); + } + } +} + +/** @public */ +export function createOriginFilter( + config: Config, +): (origin: string) => boolean { + const appUrl = config.getString('app.baseUrl'); + const { origin: appOrigin } = new URL(appUrl); + + const allowedOrigins = config.getOptionalStringArray( + 'auth.experimentalExtraAllowedOrigins', + ); + + const allowedOriginPatterns = + allowedOrigins?.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ) ?? []; + + return origin => { + if (origin === appOrigin) { + return true; + } + return allowedOriginPatterns.some(pattern => pattern.match(origin)); + }; +} diff --git a/plugins/auth-backend/src/service/index.ts b/plugins/auth-backend/src/service/index.ts new file mode 100644 index 0000000000..d26055aa59 --- /dev/null +++ b/plugins/auth-backend/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 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 { createRouter, type RouterOptions } from './router'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index a8876a7559..459c347835 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -24,24 +24,19 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { assertError, NotFoundError } from '@backstage/errors'; -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; +import { NotFoundError } from '@backstage/errors'; +import { CatalogApi } from '@backstage/catalog-client'; +import { bindOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import connectSessionKnex from 'connect-session-knex'; import passport from 'passport'; -import { Minimatch } from 'minimatch'; -import { CatalogAuthResolverContext } from '../lib/resolvers'; import { AuthDatabase } from '../database/AuthDatabase'; import { readBackstageTokenExpiration } from './readBackstageTokenExpiration'; import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { Config } from '@backstage/config'; -import { AuthProviderFactory } from '@backstage/plugin-auth-node'; - -/** @public */ -export type ProviderFactories = { [s: string]: AuthProviderFactory }; +import { ProviderFactories, bindProviderRouters } from '../providers/router'; /** @public */ export interface RouterOptions { @@ -65,10 +60,8 @@ export async function createRouter( config, discovery, database, - tokenManager, tokenFactoryAlgorithm, providerFactories = {}, - catalogApi, } = options; const router = Router(); @@ -103,6 +96,7 @@ export async function createRouter( config.getOptionalString('auth.identityTokenAlgorithm'), }); } + const secret = config.getOptionalString('auth.session.secret'); if (secret) { router.use(cookieParser(secret)); @@ -125,96 +119,31 @@ export async function createRouter( } else { router.use(cookieParser()); } + router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = options.disableDefaultProviderFactories + const providers = options.disableDefaultProviderFactories ? providerFactories : { ...defaultAuthProviderFactories, ...providerFactories, }; - const providersConfig = config.getOptionalConfig('auth.providers'); + bindProviderRouters(router, { + providers, + appUrl, + baseUrl: authUrl, + tokenIssuer, + ...options, + }); - const isOriginAllowed = createOriginFilter(config); - - for (const [providerId, providerFactory] of Object.entries( - allProviderFactories, - )) { - if (providersConfig?.has(providerId)) { - logger.info(`Configuring auth provider: ${providerId}`); - try { - const provider = providerFactory({ - providerId, - appUrl, - baseUrl: authUrl, - isOriginAllowed, - globalConfig: { - baseUrl: authUrl, - appUrl, - isOriginAllowed, - }, - config: providersConfig.getConfig(providerId), - logger, - resolverContext: CatalogAuthResolverContext.create({ - logger, - catalogApi: - catalogApi ?? new CatalogClient({ discoveryApi: discovery }), - tokenIssuer, - tokenManager, - }), - }); - - const r = Router(); - - r.get('/start', provider.start.bind(provider)); - r.get('/handler/frame', provider.frameHandler.bind(provider)); - r.post('/handler/frame', provider.frameHandler.bind(provider)); - if (provider.logout) { - r.post('/logout', provider.logout.bind(provider)); - } - if (provider.refresh) { - r.get('/refresh', provider.refresh.bind(provider)); - r.post('/refresh', provider.refresh.bind(provider)); - } - - router.use(`/${providerId}`, r); - } catch (e) { - assertError(e); - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerId} auth provider, ${e.message}`, - ); - } - - logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); - - router.use(`/${providerId}`, () => { - // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. - throw new NotFoundError( - `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + - `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + - `Check the auth backend plugin logs when the backend starts to see more details.`, - ); - }); - } - } else { - router.use(`/${providerId}`, () => { - throw new NotFoundError( - `No auth provider registered for '${providerId}'`, - ); - }); - } - } - - router.use( - createOidcRouter({ - tokenIssuer, - baseUrl: authUrl, - }), - ); + bindOidcRouter(router, { + tokenIssuer, + baseUrl: authUrl, + }); + // Gives a more helpful error message than a plain 404 router.use('/:provider/', req => { const { provider } = req.params; throw new NotFoundError(`Unknown auth provider '${provider}'`); @@ -222,27 +151,3 @@ export async function createRouter( return router; } - -/** @public */ -export function createOriginFilter( - config: Config, -): (origin: string) => boolean { - const appUrl = config.getString('app.baseUrl'); - const { origin: appOrigin } = new URL(appUrl); - - const allowedOrigins = config.getOptionalStringArray( - 'auth.experimentalExtraAllowedOrigins', - ); - - const allowedOriginPatterns = - allowedOrigins?.map( - pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), - ) ?? []; - - return origin => { - if (origin === appOrigin) { - return true; - } - return allowedOriginPatterns.some(pattern => pattern.match(origin)); - }; -} From 4642cb7ac23211407877bcd780682ce2e9478e77 Mon Sep 17 00:00:00 2001 From: Andres Mauricio Gomez P Date: Fri, 16 Feb 2024 13:28:32 -0500 Subject: [PATCH 08/46] Added support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments Signed-off-by: Andres Mauricio Gomez P --- .changeset/two-planets-beam.md | 6 + plugins/kubernetes-common/api-report.md | 2 + plugins/kubernetes-common/src/types.ts | 1 + .../kubernetes-common/src/util/response.ts | 4 + .../src/__fixtures__/2-daemonsets.json | 581 ++++++++++++++++++ .../src/components/Cluster/Cluster.tsx | 6 + .../DaemonSetsAccordions.test.tsx | 33 + .../DaemonSetsAccordions.tsx | 147 +++++ .../DaemonSetsDrawer.test.tsx | 68 ++ .../DaemonSetsAccordions/DaemonSetsDrawer.tsx | 76 +++ .../components/DaemonSetsAccordions/index.ts | 16 + .../src/hooks/GroupedResponses.ts | 1 + 12 files changed, 941 insertions(+) create mode 100644 .changeset/two-planets-beam.md create mode 100644 plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts diff --git a/.changeset/two-planets-beam.md b/.changeset/two-planets-beam.md new file mode 100644 index 0000000000..d500d22853 --- /dev/null +++ b/.changeset/two-planets-beam.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-common': patch +'@backstage/plugin-kubernetes-react': patch +--- + +Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index c1d399b196..2f46ef90e5 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -275,6 +275,8 @@ export interface GroupedResponses extends DeploymentResources { // (undocumented) customResources: any[]; // (undocumented) + daemonSets: V1DaemonSet[]; + // (undocumented) ingresses: V1Ingress[]; // (undocumented) jobs: V1Job[]; diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index cd316af089..c4bb5fd32f 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -294,4 +294,5 @@ export interface GroupedResponses extends DeploymentResources { cronJobs: V1CronJob[]; customResources: any[]; statefulsets: V1StatefulSet[]; + daemonSets: V1DaemonSet[]; } diff --git a/plugins/kubernetes-common/src/util/response.ts b/plugins/kubernetes-common/src/util/response.ts index a86d90f780..b62fa162d1 100644 --- a/plugins/kubernetes-common/src/util/response.ts +++ b/plugins/kubernetes-common/src/util/response.ts @@ -58,6 +58,9 @@ export const groupResponses = ( case 'statefulsets': prev.statefulsets.push(...next.resources); break; + case 'daemonsets': + prev.daemonSets.push(...next.resources); + break; default: } return prev; @@ -74,6 +77,7 @@ export const groupResponses = ( cronJobs: [], customResources: [], statefulsets: [], + daemonSets: [], } as GroupedResponses, ); }; diff --git a/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json b/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json new file mode 100644 index 0000000000..1c88b9d8a7 --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json @@ -0,0 +1,581 @@ +{ + "daemonSets": [ + { + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "deprecated.daemonset.template.generation": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"fluentd-elasticsearch\"}},\"spec\":{\"containers\":[{\"image\":\"quay.io/fluentd_elasticsearch/fluentd:v2.5.2\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}],\"terminationGracePeriodSeconds\":30}}}}\n" + }, + "creationTimestamp": "2024-02-14T21:00:28Z", + "generation": 1, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "k8s-app": "fluentd-logging" + }, + "name": "fluentd-elasticsearch", + "namespace": "default", + "resourceVersion": "1769498", + "uid": "2ba243f3-a733-4b63-9db9-c9b8ce303a23" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "name": "fluentd-elasticsearch" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "name": "fluentd-elasticsearch" + } + }, + "spec": { + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 0, + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 1, + "desiredNumberScheduled": 1, + "numberAvailable": 1, + "numberMisscheduled": 0, + "numberReady": 1, + "observedGeneration": 1, + "updatedNumberScheduled": 1 + } + }, + { + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "deprecated.daemonset.template.generation": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch2\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch2\"}},\"template\":{\"metadata\":{\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"name\":\"fluentd-elasticsearch2\"}},\"spec\":{\"containers\":[{\"image\":\"quay.io/fluentd_elasticsearch/fluentd:v2.5.2\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}],\"terminationGracePeriodSeconds\":30}}}}\n" + }, + "creationTimestamp": "2024-02-16T18:11:26Z", + "generation": 1, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "k8s-app": "fluentd-logging" + }, + "name": "fluentd-elasticsearch2", + "namespace": "default", + "resourceVersion": "1981736", + "uid": "b923d035-e9a4-4a40-9472-f0f3468f30df" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "name": "fluentd-elasticsearch2" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "name": "fluentd-elasticsearch2" + } + }, + "spec": { + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 0, + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 1, + "desiredNumberScheduled": 1, + "numberAvailable": 1, + "numberMisscheduled": 0, + "numberReady": 1, + "observedGeneration": 1, + "updatedNumberScheduled": 1 + } + } + ], + "pods": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2024-02-16T16:35:06Z", + "generateName": "fluentd-elasticsearch-", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-revision-hash": "86978587c8", + "name": "fluentd-elasticsearch", + "pod-template-generation": "2" + }, + "name": "fluentd-elasticsearch-mmkpf", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "DaemonSet", + "name": "fluentd-elasticsearch", + "uid": "2ba243f3-a733-4b63-9db9-c9b8ce303a23" + } + ], + "resourceVersion": "1970744", + "uid": "fc9f9070-1a3c-4e09-9cd6-8803a4ebfb0a" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchFields": [ + { + "key": "metadata.name", + "operator": "In", + "values": ["ucp-control-plane"] + } + ] + } + ] + } + } + }, + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-8cwbv", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "ucp-control-plane", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/disk-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/pid-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/unschedulable", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-8cwbv", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:06Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:08Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:08Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:06Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://958e4ec14b6a3557724fabeaac9c8f8fe2ea797337a4397e700209f6e482e898", + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imageID": "sha256:a5bf47027e067e0376708cae10750ea154b13356dfc0209610f5ea0bc7c16fe0", + "lastState": {}, + "name": "fluentd-elasticsearch", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-02-16T16:35:07Z" + } + } + } + ], + "hostIP": "172.19.0.3", + "phase": "Running", + "podIP": "10.244.0.215", + "podIPs": [ + { + "ip": "10.244.0.215" + } + ], + "qosClass": "Burstable", + "startTime": "2024-02-16T16:35:06Z" + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2024-02-16T18:11:26Z", + "generateName": "fluentd-elasticsearch2-", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-revision-hash": "5979b599f6", + "name": "fluentd-elasticsearch2", + "pod-template-generation": "1" + }, + "name": "fluentd-elasticsearch2-7hwwg", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "DaemonSet", + "name": "fluentd-elasticsearch2", + "uid": "b923d035-e9a4-4a40-9472-f0f3468f30df" + } + ], + "resourceVersion": "1981735", + "uid": "1b829903-2c3a-453f-a8f2-b6603eec0bbe" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchFields": [ + { + "key": "metadata.name", + "operator": "In", + "values": ["ucp-control-plane"] + } + ] + } + ] + } + } + }, + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-rpkbp", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "ucp-control-plane", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/disk-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/pid-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/unschedulable", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-rpkbp", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:27Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:28Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:28Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:27Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://3372cb5a300c87e5b1530b32fea7af35dce92470fc7c1cea47c714fac283feb9", + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imageID": "sha256:a5bf47027e067e0376708cae10750ea154b13356dfc0209610f5ea0bc7c16fe0", + "lastState": {}, + "name": "fluentd-elasticsearch", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-02-16T18:11:28Z" + } + } + } + ], + "hostIP": "172.19.0.3", + "phase": "Running", + "podIP": "10.244.0.88", + "podIPs": [ + { + "ip": "10.244.0.88" + } + ], + "qosClass": "Burstable", + "startTime": "2024-02-16T18:11:27Z" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx index f2108ac1b7..d713b9a873 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx @@ -34,6 +34,7 @@ import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; +import { DaemonSetsAccordions } from '../DaemonSetsAccordions'; import { ClusterContext, GroupedResponsesContext, @@ -151,6 +152,11 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { ) : undefined} + {groupedResponses.daemonSets.length > 0 ? ( + + + + ) : undefined} {groupedResponses.statefulsets.length > 0 ? ( diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx new file mode 100644 index 0000000000..9f38abf897 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 React from 'react'; +import { screen } from '@testing-library/react'; +import { DaemonSetsAccordions } from './DaemonSetsAccordions'; +import * as oneDaemonsFixture from '../../__fixtures__/2-daemonsets.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('DaemonSetsAccordions', () => { + it('should render two daemonset', async () => { + const wrapper = kubernetesProviders(oneDaemonsFixture); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('fluentd-elasticsearch')).toBeInTheDocument(); + expect(screen.getByText('fluentd-elasticsearch2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx new file mode 100644 index 0000000000..834a86e837 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx @@ -0,0 +1,147 @@ +/* + * 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 React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Grid, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1Pod, V1DaemonSet } from '@kubernetes/client-node'; +import { PodsTable } from '../Pods'; +import { DaemonSetDrawer } from './DaemonSetsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { + GroupedResponsesContext, + PodNamesWithErrorsContext, +} from '../../hooks'; +import { StatusError, StatusOK } from '@backstage/core-components'; +import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable'; + +type DaemonSetsAccordionsProps = { + children?: React.ReactNode; +}; + +type DaemonSetAccordionProps = { + daemonset: V1DaemonSet; + ownedPods: V1Pod[]; + children?: React.ReactNode; +}; + +type DaemonSetSummaryProps = { + daemonset: V1DaemonSet; + numberOfCurrentPods: number; + numberOfPodsWithErrors: number; + children?: React.ReactNode; +}; + +const DaemonSetSummary = ({ + daemonset, + numberOfCurrentPods, + numberOfPodsWithErrors, +}: DaemonSetSummaryProps) => { + return ( + + + + + + + {numberOfCurrentPods} pods + + + {numberOfPodsWithErrors > 0 ? ( + + {numberOfPodsWithErrors} pod + {numberOfPodsWithErrors > 1 ? 's' : ''} with errors + + ) : ( + No pods with errors + )} + + + + ); +}; + +const DaemonSetAccordion = ({ + daemonset, + ownedPods, +}: DaemonSetAccordionProps) => { + const podNamesWithErrors = useContext(PodNamesWithErrorsContext); + + const podsWithErrors = ownedPods.filter(p => + podNamesWithErrors.has(p.metadata?.name ?? ''), + ); + + return ( + + }> + + + + + + + ); +}; + +export const DaemonSetsAccordions = ({}: DaemonSetsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {groupedResponses.daemonSets.map((daemonset, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx new file mode 100644 index 0000000000..51e70a3b9b --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from 'react'; +import * as daemonsets from '../../__fixtures__/2-daemonsets.json'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { DaemonSetDrawer } from './DaemonSetsDrawer'; +import { kubernetesClusterLinkFormatterApiRef } from '../../api'; + +describe('DaemonsetsDrawer', () => { + it('should render daemonsets drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + + + , + , + ); + expect(getAllByText('fluentd-elasticsearch')).toHaveLength(2); + expect(getAllByText('DaemonSet')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Update Strategy Type')).toBeInTheDocument(); + expect(getByText('RollingUpdate')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('???')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('Revision History Limit')).toBeInTheDocument(); + expect(getByText('Current Number Scheduled')).toBeInTheDocument(); + expect(getByText('Desired Number Scheduled')).toBeInTheDocument(); + expect(getByText('Number Available')).toBeInTheDocument(); + expect(getByText('Number Misscheduled')).toBeInTheDocument(); + expect(getByText('Number Ready')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + }); + + it('should render deployment drawer without namespace', async () => { + const daemonset = (daemonsets as any).daemonSets[0]; + const { queryByText } = await renderInTestApp( + + + , + , + ); + + expect(queryByText('namespace: default')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx new file mode 100644 index 0000000000..e0f64ed679 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx @@ -0,0 +1,76 @@ +/* + * 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 React from 'react'; +import { V1DaemonSet } from '@kubernetes/client-node'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; +import { Typography, Grid, Chip } from '@material-ui/core'; + +export const DaemonSetDrawer = ({ + daemonset, + expanded, +}: { + daemonset: V1DaemonSet; + expanded?: boolean; +}) => { + const namespace = daemonset.metadata?.namespace; + return ( + { + return { + updateStrategyType: daemonsetObj.spec?.updateStrategy?.type ?? '???', + minReadySeconds: daemonsetObj.spec?.minReadySeconds ?? '???', + revisionHistoryLimit: + daemonsetObj.spec?.revisionHistoryLimit ?? '???', + currentNumberScheduled: + daemonsetObj.status?.currentNumberScheduled ?? '???', + desiredNumberScheduled: + daemonsetObj.status?.desiredNumberScheduled ?? '???', + numberAvailable: daemonsetObj.status?.numberAvailable ?? '???', + numberMisscheduled: daemonsetObj.status?.numberMisscheduled ?? '???', + numberReady: daemonsetObj.status?.numberReady ?? '???', + }; + }} + > + + + + {daemonset.metadata?.name ?? 'unknown object'} + + + + + DaemonSet + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts new file mode 100644 index 0000000000..ca25150cbd --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 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 { DaemonSetsAccordions } from './DaemonSetsAccordions'; diff --git a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts index 7a81a74337..10cd3a8abe 100644 --- a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts +++ b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts @@ -25,6 +25,7 @@ export const GroupedResponsesContext = React.createContext({ pods: [], replicaSets: [], deployments: [], + daemonSets: [], services: [], configMaps: [], horizontalPodAutoscalers: [], From 9aa01eeb0819fddc01d751fffd3c1a3368e5431c Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 12:42:04 +0100 Subject: [PATCH 09/46] Updated BEP 0004 Signed-off-by: bnechyporenko --- .../README.md | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/beps/0004-scaffolder-task-idempotency/README.md b/beps/0004-scaffolder-task-idempotency/README.md index a7e592af39..28b052e288 100644 --- a/beps/0004-scaffolder-task-idempotency/README.md +++ b/beps/0004-scaffolder-task-idempotency/README.md @@ -4,7 +4,8 @@ status: provisional authors: - 'bnechyporenko@bol.com' - 'benjaminl@spotify.com' -owners: +owners: + - @backstage/scaffolder-maintainers project-areas: - scaffolder creation-date: 2024-01-31 @@ -153,7 +154,7 @@ export function createGithubRepoCreateAction(options: { username: owner, }); - await ctx.checkpoint('v1.task.checkpoint.repo.creation', async () => { + await ctx.checkpoint('repo.creation', async () => { const repoCreationPromise = user.data.type === 'Organization' ? client.rest.repos.createInOrg({ @@ -168,19 +169,16 @@ export function createGithubRepoCreateAction(options: { }); if (secrets) { - await ctx.checkpoint( - 'v1.task.checkpoint.repo.create.variables', - async () => { - for (const [key, value] of Object.entries(repoVariables ?? {})) { - await client.rest.actions.createRepoVariable({ - owner, - repo, - name: key, - value: value, - }); - } - }, - ); + await ctx.checkpoint('repo.create.variables', async () => { + for (const [key, value] of Object.entries(repoVariables ?? {})) { + await client.rest.actions.createRepoVariable({ + owner, + repo, + name: key, + value: value, + }); + } + }); } ctx.output('remoteUrl', newRepo.clone_url); @@ -204,7 +202,7 @@ Checkpoints will allow action authors to create actions where code paths are ign This will be provided on a context object and action of author provide a key and a callback. ```typescript -await ctx.checkpoint('v1.task.checkpoint.repo.creation', async () => { +await ctx.checkpoint('repo.creation', async () => { const { repoUrl } = await client.rest.Repository.create({}); return { repoUrl }; }); @@ -215,7 +213,7 @@ It's going look like: ```json { - "v1.task.checkpoint.repo.creation": { + "repo.creation": { "status": "success", "result": { "repoUrl": "https://github.com/backstage/backstage.git" @@ -224,6 +222,41 @@ It's going look like: } ``` +or a failed attempt as: + +```json +{ + "repo.creation": { + "status": "failed", + "reason": "Namespace is not valid" + } +} +``` + +DatabaseTaskStore will provide two extra methods `saveTaskState` and `getTaskState`. The type of state in API will be +represented as `JsonObject`. + +Task state will be stored in the extra column `state` in the table `tasks` with the next structure: + +```json +{ + "state": { + "repo.creation": { + "status": "success", + "result": { + "repoUrl": "https://github.com/backstage/backstage.git" + } + }, + "repo.add.member": { + "status": "success", + "result": { + "id": "2345" + } + } + } +} +``` + ## Release Plan