From 6a694ce98e323bbee6ba3a7523c37ceb01badad1 Mon Sep 17 00:00:00 2001 From: ohjongsung Date: Mon, 22 May 2023 16:56:38 +0900 Subject: [PATCH 01/78] Add a scaffolder action that pull-requests for Bitbucket Server Signed-off-by: ohjongsung --- .changeset/rude-bags-count.md | 5 + .../actions/builtin/createBuiltinActions.ts | 5 + .../bitbucketServerPullRequest.test.ts | 389 ++++++++++++++++++ .../publish/bitbucketServerPullRequest.ts | 262 ++++++++++++ .../actions/builtin/publish/index.ts | 1 + 5 files changed, 662 insertions(+) create mode 100644 .changeset/rude-bags-count.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts diff --git a/.changeset/rude-bags-count.md b/.changeset/rude-bags-count.md new file mode 100644 index 0000000000..e2498f4abf --- /dev/null +++ b/.changeset/rude-bags-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add a scaffolder action that pull-requests for bitbucket server diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 747b5cdaed..28a530eb04 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -52,6 +52,7 @@ import { createPublishBitbucketAction, createPublishBitbucketCloudAction, createPublishBitbucketServerAction, + createPublishBitbucketServerPullRequestAction, createPublishGerritAction, createPublishGerritReviewAction, createPublishGithubAction, @@ -162,6 +163,10 @@ export const createBuiltinActions = ( integrations, config, }), + createPublishBitbucketServerPullRequestAction({ + integrations, + config, + }), createPublishAzureAction({ integrations, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.test.ts new file mode 100644 index 0000000000..af3999097f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.test.ts @@ -0,0 +1,389 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('../helpers', () => { + return { + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +import { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; + +describe('publish:bitbucketServer:pull-request', () => { + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'basic-auth.bitbucket.com', + username: 'test-user', + password: 'test-password', + apiBaseUrl: 'https://basic-auth.bitbucket.com/rest/api/1.0', + }, + { + host: 'no-credentials.bitbucket.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketServerPullRequestAction({ + integrations, + config, + }); + const mockContext = { + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + title: 'Add Scaffolder actions for Bitbucket Server', + description: + 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Server', + targetBranch: 'master', + sourceBranch: 'develop', + }, + workspacePath: 'wsp', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + const responseOfBranches = { + size: 3, + limit: 25, + isLastPage: true, + values: [ + { + id: 'refs/heads/master', + displayId: 'master', + type: 'BRANCH', + latestCommit: 'b1041e3f9b071b3d5cacd6826b7549cd624418f1', + latestChangeset: 'b1041e3f9b071b3d5cacd6826b7549cd624418f1', + isDefault: true, + }, + { + id: 'refs/heads/develop', + displayId: 'develop', + type: 'BRANCH', + latestCommit: '98e21148205367aeb11c25a52eaca3c2945253fa', + latestChangeset: '98e21148205367aeb11c25a52eaca3c2945253fa', + isDefault: false, + }, + { + id: 'refs/heads/develop-2', + displayId: 'develop-2', + type: 'BRANCH', + latestCommit: 'b1041e3f9b071b3d5cacd6826b7549cd624418f1', + latestChangeset: 'b1041e3f9b071b3d5cacd6826b7549cd624418f1', + isDefault: false, + }, + ], + start: 0, + }; + const responseOfPullRequests = { + id: 19, + version: 0, + title: 'Test for bitbucket server pull-requests', + description: 'Test for bitbucket server pull-requests', + state: 'OPEN', + open: true, + closed: false, + createdDate: 1684200289521, + updatedDate: 1684200289521, + fromRef: { + id: 'refs/heads/develop', + displayId: 'develop', + latestCommit: '98e21148205367aeb11c25a52eaca3c2945253fa', + type: 'BRANCH', + repository: { + slug: 'repo', + id: 1812, + name: 'repo', + description: 'This is a test repo', + hierarchyId: '1da8822903a9b11a27b8', + scmId: 'git', + state: 'AVAILABLE', + statusMessage: 'Available', + forkable: true, + project: {}, + public: false, + links: {}, + }, + }, + toRef: { + id: 'refs/heads/master', + displayId: 'master', + latestCommit: 'b1041e3f9b071b3d5cacd6826b7549cd624418f1', + type: 'BRANCH', + repository: { + slug: 'repo', + id: 1812, + name: 'repo', + description: 'This is a test repo', + hierarchyId: '1da8822903a9b11a27b8', + scmId: 'git', + state: 'AVAILABLE', + statusMessage: 'Available', + forkable: true, + project: {}, + public: false, + links: {}, + }, + }, + locked: false, + author: { + user: { + name: 'test-user', + emailAddress: 'test-user@sample.com', + id: 2944, + displayName: 'test-user', + active: true, + slug: 'test-user', + type: 'NORMAL', + links: {}, + }, + role: 'AUTHOR', + approved: false, + status: 'UNAPPROVED', + }, + reviewers: [], + participants: [], + links: { + self: [ + { + href: 'https://hosted.bitbucket.com/projects/project/repos/repo/pull-requests/1', + }, + ], + }, + }; + const handlers = [ + rest.get( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos/repo/branches', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos/repo/pull-requests', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ]; + + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?repo=repo', + }, + }), + ).rejects.toThrow(/missing project/); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?project=project', + }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should throw if there is no integration config provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'missing.com?project=project&repo=repo', + }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); + + it('should throw if there no credentials in the integration config that is returned', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'no-credentials.bitbucket.com?project=project&repo=repo', + }, + }), + ).rejects.toThrow( + /Authorization has not been provided for no-credentials.bitbucket.com/, + ); + }); + + it('should call the correct APIs with token', async () => { + expect.assertions(3); + server.use( + rest.get( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos/repo/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos/repo/pull-requests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + }, + }); + }); + + it('should call the correct APIs with basic auth', async () => { + expect.assertions(3); + server.use( + rest.get( + 'https://basic-auth.bitbucket.com/rest/api/1.0/projects/project/repos/repo/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://basic-auth.bitbucket.com/rest/api/1.0/projects/project/repos/repo/pull-requests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'basic-auth.bitbucket.com?project=project&repo=repo', + }, + }); + }); + + it('should work if the token is provided through ctx.input', async () => { + expect.assertions(3); + const token = 'user-token'; + server.use( + rest.get( + 'https://no-credentials.bitbucket.com/rest/api/1.0/projects/project/repos/repo/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://no-credentials.bitbucket.com/rest/api/1.0/projects/project/repos/repo/pull-requests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'no-credentials.bitbucket.com?project=project&repo=repo', + token: token, + }, + }); + }); + + it('should call outputs with the correct urls', async () => { + server.use(...handlers); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pullRequestUrl', + 'https://hosted.bitbucket.com/projects/project/repos/repo/pull-requests/1', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts new file mode 100644 index 0000000000..cfcccb05ce --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts @@ -0,0 +1,262 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { + getBitbucketServerRequestOptions, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import fetch, { RequestInit, Response } from 'node-fetch'; +import { parseRepoUrl } from './util'; +import { Config } from '@backstage/config'; + +const createPullRequest = async (opts: { + project: string; + repo: string; + title: string; + description?: string; + toRef: object; + fromRef: object; + authorization: string; + apiBaseUrl: string; +}) => { + const { + project, + repo, + title, + description, + toRef, + fromRef, + authorization, + apiBaseUrl, + } = opts; + + let response: Response; + const data: RequestInit = { + method: 'POST', + body: JSON.stringify({ + title: title, + description: description, + state: 'OPEN', + open: true, + closed: false, + locked: true, + toRef: toRef, + fromRef: fromRef, + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/projects/${project}/repos/${repo}/pull-requests`, + data, + ); + } catch (e) { + throw new Error(`Unable to create pull-reqeusts, ${e}`); + } + + if (response.status !== 201) { + throw new Error( + `Unable to create pull requests, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + return `${r.links.self[0].href}`; +}; +const findBranches = async (opts: { + project: string; + repo: string; + branchName: string; + authorization: string; + apiBaseUrl: string; +}) => { + const { project, repo, branchName, authorization, apiBaseUrl } = opts; + + let response: Response; + const options: RequestInit = { + method: 'GET', + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/projects/${project}/repos/${repo}/branches?boostMatches=true&filterText=${branchName}`, + options, + ); + } catch (e) { + throw new Error(`Unable to get branches, ${e}`); + } + + if (response.status !== 200) { + throw new Error( + `Unable to get branches, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + let branch; + for (const object of r.values) { + if (object.displayId === branchName) { + branch = object; + break; + } + } + + return branch; +}; + +export function createPublishBitbucketServerPullRequestAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations } = options; + + return createTemplateAction<{ + repoUrl: string; + title: string; + description?: string; + targetBranch: string; + sourceBranch: string; + token?: string; + }>({ + id: 'publish:bitbucketServer:pull-request', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + title: { + title: 'Pull Request Name', + type: 'string', + description: 'The name for the pull request', + }, + description: { + title: 'Pull Request Description', + type: 'string', + description: 'The description of the pull request', + }, + targetBranch: { + title: 'Target Branch', + type: 'string', + description: 'The description of the pull request', + }, + sourceBranch: { + title: 'Source Branch', + type: 'string', + description: 'The description of the pull request', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: + 'The token to use for authorization to BitBucket Server', + }, + }, + }, + output: { + type: 'object', + properties: { + pullRequestUrl: { + title: 'A URL to the pull request with the provider', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { repoUrl, title, description, targetBranch, sourceBranch } = + ctx.input; + + const { project, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!project) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, + ); + } + + const integrationConfig = integrations.bitbucketServer.byHost(host); + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const token = ctx.input.token ?? integrationConfig.config.token; + + const authConfig = { + ...integrationConfig.config, + ...{ token }, + }; + + const reqOpts = getBitbucketServerRequestOptions(authConfig); + const authorization = reqOpts.headers.Authorization; + if (!authorization) { + throw new Error( + `Authorization has not been provided for ${integrationConfig.config.host}. Please add either (a) a user login auth token, or (b) a token or (c) username + password to the integration config.`, + ); + } + + const apiBaseUrl = integrationConfig.config.apiBaseUrl; + + const toRef = await findBranches({ + project, + repo, + branchName: targetBranch, + authorization, + apiBaseUrl, + }); + + const fromRef = await findBranches({ + project, + repo, + branchName: sourceBranch, + authorization, + apiBaseUrl, + }); + + const pullRequestUrl = await createPullRequest({ + project, + repo, + title, + description, + toRef, + fromRef, + authorization, + apiBaseUrl, + }); + + ctx.output('pullRequestUrl', pullRequestUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index a8a40ab1df..bed3a2316c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -18,6 +18,7 @@ export { createPublishAzureAction } from './azure'; export { createPublishBitbucketAction } from './bitbucket'; export { createPublishBitbucketCloudAction } from './bitbucketCloud'; export { createPublishBitbucketServerAction } from './bitbucketServer'; +export { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; export { createPublishGerritAction } from './gerrit'; export { createPublishGerritReviewAction } from './gerritReview'; export { createPublishGithubAction } from './github'; From e76e0391fe5c9ce15dccb92bcd53309e3a282887 Mon Sep 17 00:00:00 2001 From: ohjongsung Date: Tue, 23 May 2023 10:27:01 +0900 Subject: [PATCH 02/78] Generate API reports Signed-off-by: ohjongsung --- plugins/scaffolder-backend/api-report.md | 16 ++++++++++++++++ .../publish/bitbucketServerPullRequest.ts | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 07dd5eaf1d..aed57e5bc2 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -448,6 +448,22 @@ export function createPublishBitbucketServerAction(options: { JsonObject >; +// @public +export function createPublishBitbucketServerPullRequestAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}): TemplateAction_2< + { + repoUrl: string; + title: string; + description?: string | undefined; + targetBranch: string; + sourceBranch: string; + token?: string | undefined; + }, + JsonObject +>; + // @public export function createPublishGerritAction(options: { integrations: ScmIntegrationRegistry; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts index cfcccb05ce..4389e3a424 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts @@ -131,6 +131,10 @@ const findBranches = async (opts: { return branch; }; +/** + * Creates a BitbucketServer Pull Request action. + * @public + */ export function createPublishBitbucketServerPullRequestAction(options: { integrations: ScmIntegrationRegistry; config: Config; From 8693a913dec637a16bd2d9451d5b924f3b9d1bbe Mon Sep 17 00:00:00 2001 From: Hayden Spitzley Date: Thu, 25 May 2023 13:10:53 -0400 Subject: [PATCH 03/78] feat: allow making isLocationMatch use exact match in query string matcher Signed-off-by: Hayden Spitzley --- .../src/layout/Sidebar/SidebarSubmenuItem.tsx | 7 ++--- .../src/layout/Sidebar/utils.test.ts | 26 +++++++++++++++++++ .../src/layout/Sidebar/utils.ts | 10 +++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index b08647d335..7ec0a40624 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -130,6 +130,7 @@ export type SidebarSubmenuItemProps = { to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; + exact?: boolean; }; /** @@ -138,7 +139,7 @@ export type SidebarSubmenuItemProps = { * @public */ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { - const { title, subtitle, to, icon: Icon, dropdownItems } = props; + const { title, subtitle, to, icon: Icon, dropdownItems, exact } = props; const classes = useStyles(); const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext); const closeSubmenu = () => { @@ -146,7 +147,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { }; const toLocation = useResolvedPath(to ?? ''); const currentLocation = useLocation(); - let isActive = isLocationMatch(currentLocation, toLocation); + let isActive = isLocationMatch(currentLocation, toLocation, exact || false); const [showDropDown, setShowDropDown] = useState(false); const handleClickDropdown = () => { @@ -155,7 +156,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { if (dropdownItems !== undefined) { dropdownItems.some(item => { const resolvedPath = resolvePath(item.to); - isActive = isLocationMatch(currentLocation, resolvedPath); + isActive = isLocationMatch(currentLocation, resolvedPath, exact || false); return isActive; }); return ( diff --git a/packages/core-components/src/layout/Sidebar/utils.test.ts b/packages/core-components/src/layout/Sidebar/utils.test.ts index b08dcda0fc..976108a63d 100644 --- a/packages/core-components/src/layout/Sidebar/utils.test.ts +++ b/packages/core-components/src/layout/Sidebar/utils.test.ts @@ -95,4 +95,30 @@ describe('isLocationMatching', () => { toLocation = { pathname: '/catalog', search: '', hash: '' }; expect(isLocationMatch(currentLocation, toLocation)).toBe(true); }); + + describe('exact matching', () => { + it('return false when target query parameters are subset of current location query parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?x=foo&y=bar', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation, true)).toBe(false); + }); + + it('return true when target query parameters are exact match with current location query parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?x=foo&y=bar', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?y=bar&x=foo', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation, true)).toBe(true); + }); + }); }); diff --git a/packages/core-components/src/layout/Sidebar/utils.ts b/packages/core-components/src/layout/Sidebar/utils.ts index 1afebe796d..5dba73f4cd 100644 --- a/packages/core-components/src/layout/Sidebar/utils.ts +++ b/packages/core-components/src/layout/Sidebar/utils.ts @@ -18,7 +18,11 @@ import { Location, Path } from 'history'; import { isEqual, isMatch } from 'lodash'; import qs from 'qs'; -export function isLocationMatch(currentLocation: Location, toLocation: Path) { +export function isLocationMatch( + currentLocation: Location, + toLocation: Path, + exact: boolean = false, +) { const toDecodedSearch = new URLSearchParams(toLocation.search).toString(); const toQueryParameters = qs.parse(toDecodedSearch); @@ -27,9 +31,11 @@ export function isLocationMatch(currentLocation: Location, toLocation: Path) { ).toString(); const currentQueryParameters = qs.parse(currentDecodedSearch); + const queryStringMatcher = exact ? isEqual : isMatch; + const matching = isEqual(toLocation.pathname, currentLocation.pathname) && - isMatch(currentQueryParameters, toQueryParameters); + queryStringMatcher(currentQueryParameters, toQueryParameters); return matching; } From c1f1eebe36d55746cf95ab5ca1098c1458d893bc Mon Sep 17 00:00:00 2001 From: Maapteh Date: Mon, 29 May 2023 13:56:51 +0200 Subject: [PATCH 04/78] fix: start with some printable containers Signed-off-by: Maapteh --- packages/core-components/src/layout/Page/Page.tsx | 5 +++++ packages/core-components/src/layout/Sidebar/Bar.tsx | 3 +++ .../core-components/src/layout/Sidebar/MobileSidebar.tsx | 3 +++ packages/core-components/src/layout/Sidebar/Page.tsx | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/packages/core-components/src/layout/Page/Page.tsx b/packages/core-components/src/layout/Page/Page.tsx index 00988c475b..e388e0366a 100644 --- a/packages/core-components/src/layout/Page/Page.tsx +++ b/packages/core-components/src/layout/Page/Page.tsx @@ -33,6 +33,11 @@ const useStyles = makeStyles( [theme.breakpoints.down('xs')]: { height: '100%', }, + '@media print': { + display: 'block', + height: 'auto', + overflowY: 'inherit', + }, }, }), { name: 'BackstagePage' }, diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 815d6bc0d6..8cb43ba007 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -63,6 +63,9 @@ const useStyles = makeStyles( '&::-webkit-scrollbar': { display: 'none', }, + '@media print': { + display: 'none', + }, }, drawerWidth: props => ({ width: props.sidebarConfig.drawerWidthClosed, diff --git a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx index 9000c2f10b..bf96703613 100644 --- a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx +++ b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx @@ -72,6 +72,9 @@ const useStyles = makeStyles( zIndex: theme.zIndex.snackbar, // SidebarDivider color borderTop: '1px solid #383838', + '@media print': { + display: 'none', + }, }, overlay: props => ({ diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index 5f660547d1..09935867b3 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -51,6 +51,10 @@ const useStyles = makeStyles< [theme.breakpoints.down('xs')]: { paddingBottom: props => props.sidebarConfig.mobileSidebarHeight, }, + '@media print': { + paddingLeft: '0px !important', + paddingBottom: '0px !important', + }, }, content: { zIndex: 0, From 36bedda8da9a3485d264f70c701ccea664911bb1 Mon Sep 17 00:00:00 2001 From: Maapteh Date: Mon, 29 May 2023 14:22:28 +0200 Subject: [PATCH 05/78] -s fix: each container not breaked in start... Signed-off-by: Maapteh --- packages/core-components/src/layout/InfoCard/InfoCard.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 3d8d0f3bb0..3034ce1cf2 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -92,6 +92,10 @@ const VARIANT_STYLES = { flexDirection: 'column', height: 'calc(100% - 10px)', // for pages without content header marginBottom: '10px', + breakInside: 'avoid', + '@media print': { + height: 'auto', + }, }, }, cardContent: { From e33beb1f2a8ef53bfc18339630eecf9c2ced45bb Mon Sep 17 00:00:00 2001 From: Maapteh Date: Mon, 29 May 2023 14:40:16 +0200 Subject: [PATCH 06/78] add changelog Signed-off-by: Maapteh --- .changeset/smart-ads-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/smart-ads-rush.md diff --git a/.changeset/smart-ads-rush.md b/.changeset/smart-ads-rush.md new file mode 100644 index 0000000000..4f7cad0d49 --- /dev/null +++ b/.changeset/smart-ads-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Make the documentation pages printable (also handy for exporting to pdf when go-green) From 69db3a7e9427da205342e33d81ef755e5620d730 Mon Sep 17 00:00:00 2001 From: Maapteh Date: Mon, 29 May 2023 20:13:43 +0200 Subject: [PATCH 07/78] review Signed-off-by: Maapteh --- .changeset/smart-ads-rush.md | 2 +- packages/core-components/src/layout/Sidebar/Page.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/smart-ads-rush.md b/.changeset/smart-ads-rush.md index 4f7cad0d49..45ed2ff593 100644 --- a/.changeset/smart-ads-rush.md +++ b/.changeset/smart-ads-rush.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Make the documentation pages printable (also handy for exporting to pdf when go-green) +Make the documentation pages printable (also handy for exporting to PDF) diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index 09935867b3..c0107256e1 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -52,8 +52,7 @@ const useStyles = makeStyles< paddingBottom: props => props.sidebarConfig.mobileSidebarHeight, }, '@media print': { - paddingLeft: '0px !important', - paddingBottom: '0px !important', + padding: '0px !important', }, }, content: { From f242762235ac2c0bf27498e7f782a50b3d75a994 Mon Sep 17 00:00:00 2001 From: Maapteh Date: Mon, 29 May 2023 20:16:08 +0200 Subject: [PATCH 08/78] review Signed-off-by: Maapteh --- packages/core-components/src/layout/InfoCard/InfoCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 3034ce1cf2..c80e71b609 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -92,7 +92,7 @@ const VARIANT_STYLES = { flexDirection: 'column', height: 'calc(100% - 10px)', // for pages without content header marginBottom: '10px', - breakInside: 'avoid', + breakInside: 'avoid-page', '@media print': { height: 'auto', }, From 485c49192b2544d05a51ad29e7fe6ddee0403007 Mon Sep 17 00:00:00 2001 From: Hayden Spitzley Date: Tue, 30 May 2023 09:01:58 -0400 Subject: [PATCH 09/78] remove fallback value and update api report Signed-off-by: Hayden Spitzley --- packages/core-components/api-report.md | 1 + .../core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 9f4aca1dcf..df34abdd03 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1087,6 +1087,7 @@ export type SidebarSubmenuItemProps = { to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; + exact?: boolean; }; // @public diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index 7ec0a40624..d05ee23636 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -147,7 +147,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { }; const toLocation = useResolvedPath(to ?? ''); const currentLocation = useLocation(); - let isActive = isLocationMatch(currentLocation, toLocation, exact || false); + let isActive = isLocationMatch(currentLocation, toLocation, exact); const [showDropDown, setShowDropDown] = useState(false); const handleClickDropdown = () => { @@ -156,7 +156,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { if (dropdownItems !== undefined) { dropdownItems.some(item => { const resolvedPath = resolvePath(item.to); - isActive = isLocationMatch(currentLocation, resolvedPath, exact || false); + isActive = isLocationMatch(currentLocation, resolvedPath, exact); return isActive; }); return ( From 66ae4d8ca380104da66c08e1c86aeb8a444c3efe Mon Sep 17 00:00:00 2001 From: Hayden Spitzley Date: Tue, 30 May 2023 09:48:50 -0400 Subject: [PATCH 10/78] add changeset Signed-off-by: Hayden Spitzley --- .changeset/perfect-donkeys-cry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-donkeys-cry.md diff --git a/.changeset/perfect-donkeys-cry.md b/.changeset/perfect-donkeys-cry.md new file mode 100644 index 0000000000..83eafe69ca --- /dev/null +++ b/.changeset/perfect-donkeys-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add option to require exact matching for isLocationMatch From 7493e042757efc83b45a192c8b58eedf530289a3 Mon Sep 17 00:00:00 2001 From: Maapteh Date: Tue, 30 May 2023 17:33:12 +0200 Subject: [PATCH 11/78] can not run techdocs maybe this is it.. Signed-off-by: Maapteh --- .../src/reader/transformers/styles/rules/layout.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts index c5e69e6084..24adabfcec 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts @@ -227,4 +227,14 @@ export default ({ theme, sidebar }: RuleOptions) => ` width: 12.1rem; } } + + +@media print { + .md-sidebar { + display: none; + } + .md-content { + margin: 0; + } +} `; From 4e15516aaf6364ed20592009a4d9636decb746fb Mon Sep 17 00:00:00 2001 From: Maapteh Date: Tue, 30 May 2023 17:38:10 +0200 Subject: [PATCH 12/78] only able to run e2e fixture documentation Signed-off-by: Maapteh --- .../techdocs/src/reader/transformers/styles/rules/layout.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts index 24adabfcec..4ab90e91e1 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts @@ -230,11 +230,14 @@ export default ({ theme, sidebar }: RuleOptions) => ` @media print { - .md-sidebar { + .md-sidebar, + #toggle-sidebar { display: none; } .md-content { margin: 0; + width: 100%; + max-width: 100%; } } `; From c695c3b5eaec1b4b3f3ed22caa755aaf4b310eb3 Mon Sep 17 00:00:00 2001 From: Maapteh Date: Tue, 30 May 2023 17:39:46 +0200 Subject: [PATCH 13/78] only able to run e2e fixture documentation Signed-off-by: Maapteh --- .changeset/smart-ads-rush.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/smart-ads-rush.md b/.changeset/smart-ads-rush.md index 45ed2ff593..369e5ab25b 100644 --- a/.changeset/smart-ads-rush.md +++ b/.changeset/smart-ads-rush.md @@ -1,5 +1,6 @@ --- '@backstage/core-components': patch +'@backstage/plugin-techdocs': patch --- Make the documentation pages printable (also handy for exporting to PDF) From 42132761151a3d90c588a9cd127838fca2456018 Mon Sep 17 00:00:00 2001 From: Maapteh Date: Tue, 30 May 2023 17:44:05 +0200 Subject: [PATCH 14/78] only able to run e2e fixture documentation Signed-off-by: Maapteh --- .../TechDocsReaderPageContent/TechDocsReaderPageContent.tsx | 3 +++ .../TechDocsReaderPageSubheader.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index 000a1ba275..1e5459fc9d 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -39,6 +39,9 @@ const useStyles = makeStyles({ width: 'calc(100% - 34.4rem)', margin: '0 auto', }, + '@media print': { + display: 'none', + }, }, }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageSubheader/TechDocsReaderPageSubheader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageSubheader/TechDocsReaderPageSubheader.tsx index f3612aa46e..85f04655fd 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageSubheader/TechDocsReaderPageSubheader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageSubheader/TechDocsReaderPageSubheader.tsx @@ -39,6 +39,9 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'column', minHeight: 'auto', padding: theme.spacing(3, 3, 0), + '@media print': { + display: 'none', + }, }, })); From c0806fe2b2e93f335264e411dd5b748fd195828a Mon Sep 17 00:00:00 2001 From: Maapteh Date: Tue, 30 May 2023 17:49:42 +0200 Subject: [PATCH 15/78] only able to run e2e fixture documentation Signed-off-by: Maapteh --- plugins/techdocs/src/reader/transformers/styles/rules/layout.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts index 4ab90e91e1..cce6af7d6f 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts @@ -234,6 +234,7 @@ export default ({ theme, sidebar }: RuleOptions) => ` #toggle-sidebar { display: none; } + .md-content { margin: 0; width: 100%; From 0eede913b7d9d1705944af875b32fc00ba9731e0 Mon Sep 17 00:00:00 2001 From: Hayden Spitzley <105455169+hspitzley-czi@users.noreply.github.com> Date: Thu, 1 Jun 2023 09:22:13 -0400 Subject: [PATCH 16/78] Update .changeset/perfect-donkeys-cry.md Co-authored-by: Patrik Oldsberg Signed-off-by: Hayden Spitzley <105455169+hspitzley-czi@users.noreply.github.com> --- .changeset/perfect-donkeys-cry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/perfect-donkeys-cry.md b/.changeset/perfect-donkeys-cry.md index 83eafe69ca..744dc19d64 100644 --- a/.changeset/perfect-donkeys-cry.md +++ b/.changeset/perfect-donkeys-cry.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Add option to require exact matching for isLocationMatch +Added `exact` prop to `SidebarSubmenuItem` which causes it to only highlight if the current location is an exact match. From 5ecc3d7865f81e6989064d4543c1f9eca56d980f Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 1 Jun 2023 16:37:40 -0500 Subject: [PATCH 17/78] fix(catalog-import): Fix stepper component order. Signed-off-by: Carlos Esteban Lopez --- .../components/DefaultImportPage/DefaultImportPage.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index 30cce9603a..f0c9d3c33a 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -47,14 +47,14 @@ export const DefaultImportPage = () => { - - - - - + + + + + From 309d2973adb4ef526fc0bd58508caefe19b2a029 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 1 Jun 2023 16:45:09 -0500 Subject: [PATCH 18/78] fix(catalog-import): Add changeset Signed-off-by: Carlos Esteban Lopez --- .changeset/cold-pillows-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-pillows-mix.md diff --git a/.changeset/cold-pillows-mix.md b/.changeset/cold-pillows-mix.md new file mode 100644 index 0000000000..12dc9422aa --- /dev/null +++ b/.changeset/cold-pillows-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Swap `ImportStepper` and `InfoCard` order to fix tab order in `catalog-import`. From 1aecd7e3811c27841ef849a4c35f94f156b1d111 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 1 Jun 2023 17:24:36 -0500 Subject: [PATCH 19/78] fix(catalog-import): Use media queries to sort content order Signed-off-by: Carlos Esteban Lopez --- .../DefaultImportPage/DefaultImportPage.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index f0c9d3c33a..c777953808 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -22,7 +22,7 @@ import { SupportButton, } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { Grid } from '@material-ui/core'; +import { Grid, useMediaQuery, useTheme } from '@material-ui/core'; import React from 'react'; import { ImportInfoCard } from '../ImportInfoCard'; import { ImportStepper } from '../ImportStepper'; @@ -33,9 +33,21 @@ import { ImportStepper } from '../ImportStepper'; * @public */ export const DefaultImportPage = () => { + const theme = useTheme(); const configApi = useApi(configApiRef); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const appTitle = configApi.getOptional('app.title') || 'Backstage'; + const contentItems = [ + + + , + + + + , + ]; + return (
@@ -48,13 +60,7 @@ export const DefaultImportPage = () => { - - - - - - - + {isMobile ? contentItems : contentItems.reverse()} From 47b01e4c5315fce331a114a1ccda06badd883f98 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 1 Jun 2023 19:45:34 -0500 Subject: [PATCH 20/78] fix(catalog): EntityContextMenu now focus visible the first element. Signed-off-by: Carlos Esteban Lopez --- .changeset/warm-hornets-fetch.md | 5 +++++ .../src/components/EntityContextMenu/EntityContextMenu.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/warm-hornets-fetch.md diff --git a/.changeset/warm-hornets-fetch.md b/.changeset/warm-hornets-fetch.md new file mode 100644 index 0000000000..eb84a72f0a --- /dev/null +++ b/.changeset/warm-hornets-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Keyboard navigation to open EntityContextMenu now focus visible the first element. diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 1961fe6cac..5e6e8ff3a9 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -142,7 +142,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { transformOrigin={{ vertical: 'top', horizontal: 'right' }} aria-labelledby="long-menu" > - + {extraItems} Date: Fri, 2 Jun 2023 16:02:58 +0900 Subject: [PATCH 21/78] Update Code and description based on feedback Signed-off-by: ohjongsung --- plugins/scaffolder-backend/api-report.md | 2 +- .../publish/bitbucketServerPullRequest.ts | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index aed57e5bc2..5099ae18b8 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -457,7 +457,7 @@ export function createPublishBitbucketServerPullRequestAction(options: { repoUrl: string; title: string; description?: string | undefined; - targetBranch: string; + targetBranch?: string | undefined; sourceBranch: string; token?: string | undefined; }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts index 4389e3a424..a0ac0e20ac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServerPullRequest.ts @@ -120,15 +120,13 @@ const findBranches = async (opts: { } const r = await response.json(); - let branch; for (const object of r.values) { if (object.displayId === branchName) { - branch = object; - break; + return object; } } - return branch; + return undefined; }; /** @@ -145,7 +143,7 @@ export function createPublishBitbucketServerPullRequestAction(options: { repoUrl: string; title: string; description?: string; - targetBranch: string; + targetBranch?: string; sourceBranch: string; token?: string; }>({ @@ -153,16 +151,16 @@ export function createPublishBitbucketServerPullRequestAction(options: { schema: { input: { type: 'object', - required: ['repoUrl'], + required: ['repoUrl', 'title', 'sourceBranch'], properties: { repoUrl: { title: 'Repository Location', type: 'string', }, title: { - title: 'Pull Request Name', + title: 'Pull Request title', type: 'string', - description: 'The name for the pull request', + description: 'The title for the pull request', }, description: { title: 'Pull Request Description', @@ -172,15 +170,15 @@ export function createPublishBitbucketServerPullRequestAction(options: { targetBranch: { title: 'Target Branch', type: 'string', - description: 'The description of the pull request', + description: `Branch of repository to apply changes to. The default value is 'master'`, }, sourceBranch: { title: 'Source Branch', type: 'string', - description: 'The description of the pull request', + description: 'Branch of repository to copy changes from', }, token: { - title: 'Authentication Token', + title: 'Authorization Token', type: 'string', description: 'The token to use for authorization to BitBucket Server', @@ -198,8 +196,13 @@ export function createPublishBitbucketServerPullRequestAction(options: { }, }, async handler(ctx) { - const { repoUrl, title, description, targetBranch, sourceBranch } = - ctx.input; + const { + repoUrl, + title, + description, + targetBranch = 'master', + sourceBranch, + } = ctx.input; const { project, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -227,7 +230,7 @@ export function createPublishBitbucketServerPullRequestAction(options: { const authorization = reqOpts.headers.Authorization; if (!authorization) { throw new Error( - `Authorization has not been provided for ${integrationConfig.config.host}. Please add either (a) a user login auth token, or (b) a token or (c) username + password to the integration config.`, + `Authorization has not been provided for ${integrationConfig.config.host}. Please add either (a) a user login auth token, or (b) a token input from the template or (c) username + password to the integration config.`, ); } From d0f5b0c886c2a554892514c6bf15e576adaa711f Mon Sep 17 00:00:00 2001 From: mingfu Date: Fri, 2 Jun 2023 21:31:46 +0800 Subject: [PATCH 22/78] set the expiration time for the oidc idToken Signed-off-by: mingfu --- .changeset/nice-peaches-reflect.md | 5 ++++ .../auth-backend/src/lib/session/constants.ts | 19 ++++++++++++ plugins/auth-backend/src/lib/session/index.ts | 17 +++++++++++ .../src/providers/github/provider.ts | 4 +-- .../src/providers/microsoft/provider.ts | 3 +- .../src/providers/oidc/provider.ts | 30 ++++++++++++------- plugins/auth-backend/src/service/router.ts | 3 +- 7 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 .changeset/nice-peaches-reflect.md create mode 100644 plugins/auth-backend/src/lib/session/constants.ts create mode 100644 plugins/auth-backend/src/lib/session/index.ts diff --git a/.changeset/nice-peaches-reflect.md b/.changeset/nice-peaches-reflect.md new file mode 100644 index 0000000000..45c9f0ed6f --- /dev/null +++ b/.changeset/nice-peaches-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Set the expiration time of oidc `idToken` to be less than backstage session expiration time. diff --git a/plugins/auth-backend/src/lib/session/constants.ts b/plugins/auth-backend/src/lib/session/constants.ts new file mode 100644 index 0000000000..0d14987ab2 --- /dev/null +++ b/plugins/auth-backend/src/lib/session/constants.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// BACKSTAGE_SESSION_EXPIRATION the default session expiration time +// TODO: find a less hard-coded way to access this, perhaps by reading it from the configuration. +export const BACKSTAGE_SESSION_EXPIRATION = 3600; diff --git a/plugins/auth-backend/src/lib/session/index.ts b/plugins/auth-backend/src/lib/session/index.ts new file mode 100644 index 0000000000..306d483ae0 --- /dev/null +++ b/plugins/auth-backend/src/lib/session/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BACKSTAGE_SESSION_EXPIRATION } from './constants'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 7571b4a327..11a71ca018 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -42,12 +42,10 @@ import { OAuthRefreshRequest, } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session'; const ACCESS_TOKEN_PREFIX = 'access-token.'; -// TODO(Rugvip): Auth providers need a way to access this in a less hardcoded way -const BACKSTAGE_SESSION_EXPIRATION = 3600; - type PrivateInfo = { refreshToken?: string; }; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 5d02b7ef61..bc581f6d10 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -51,8 +51,7 @@ import { Logger } from 'winston'; import fetch from 'node-fetch'; import { decodeJwt } from 'jose'; import { Profile as PassportProfile } from 'passport'; - -const BACKSTAGE_SESSION_EXPIRATION = 3600; +import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index a5196f0930..ed59611f21 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -48,6 +48,7 @@ import { commonByEmailLocalPartResolver, commonByEmailResolver, } from '../resolvers'; +import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session'; type PrivateInfo = { refreshToken?: string; @@ -179,17 +180,15 @@ export class OidcAuthProvider implements OAuthHandlers { // Then populate the profile with it private async handleResult(result: OidcAuthResult): Promise { const { profile } = await this.authHandler(result, this.resolverContext); - const response: OAuthResponse = { - providerInfo: { - idToken: result.tokenset.id_token, - accessToken: result.tokenset.access_token!, - scope: result.tokenset.scope!, - expiresInSeconds: result.tokenset.expires_in, - }, - profile, - }; + + const expiresInSeconds = + result.tokenset.expires_in === undefined + ? BACKSTAGE_SESSION_EXPIRATION + : Math.min(result.tokenset.expires_in, BACKSTAGE_SESSION_EXPIRATION); + + let backstageIdentity = undefined; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + backstageIdentity = await this.signInResolver( { result, profile, @@ -198,7 +197,16 @@ export class OidcAuthProvider implements OAuthHandlers { ); } - return response; + return { + backstageIdentity, + providerInfo: { + idToken: result.tokenset.id_token, + accessToken: result.tokenset.access_token!, + scope: result.tokenset.scope!, + expiresInSeconds, + }, + profile, + }; } } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 2996ff68be..fa8dd95727 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -37,6 +37,7 @@ import passport from 'passport'; import { Minimatch } from 'minimatch'; import { CatalogAuthResolverContext } from '../lib/resolvers'; import { AuthDatabase } from '../database/AuthDatabase'; +import { BACKSTAGE_SESSION_EXPIRATION } from '../lib/session'; /** @public */ export type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -77,7 +78,7 @@ export async function createRouter( logger, database: authDb, }); - const keyDurationSeconds = 3600; + const keyDurationSeconds = BACKSTAGE_SESSION_EXPIRATION; const tokenIssuer = new TokenFactory({ issuer: authUrl, From 1948845861b0950576da5dd32093f6773e198f5d Mon Sep 17 00:00:00 2001 From: Andrew Ochsner Date: Fri, 2 Jun 2023 10:04:07 -0500 Subject: [PATCH 23/78] add github:environment:create and github:deployKey:create Signed-off-by: Andrew Ochsner --- .changeset/strange-dolls-decide.md | 5 + docs/integrations/github/github-apps.md | 1 + plugins/scaffolder-backend/api-report.md | 44 +++ .../actions/builtin/createBuiltinActions.ts | 8 + .../builtin/github/githubDeployKey.test.ts | 112 ++++++++ .../actions/builtin/github/githubDeployKey.ts | 157 +++++++++++ .../builtin/github/githubEnvironment.test.ts | 258 ++++++++++++++++++ .../builtin/github/githubEnvironment.ts | 200 ++++++++++++++ .../actions/builtin/github/helpers.ts | 4 +- .../actions/builtin/github/index.ts | 2 + 10 files changed, 789 insertions(+), 2 deletions(-) create mode 100644 .changeset/strange-dolls-decide.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts diff --git a/.changeset/strange-dolls-decide.md b/.changeset/strange-dolls-decide.md new file mode 100644 index 0000000000..97dc99d2c9 --- /dev/null +++ b/.changeset/strange-dolls-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added `github:deployKey:create` and `github:environment:create` scaffolder actions. You will need to add `read/write` permissions to your GITHUB_TOKEN and/or Github Backstage App for Repository `Administration` (for deploy key functionality) and `Environments` (for Environment functionality) diff --git a/docs/integrations/github/github-apps.md b/docs/integrations/github/github-apps.md index 3cad69f56d..cc84704799 100644 --- a/docs/integrations/github/github-apps.md +++ b/docs/integrations/github/github-apps.md @@ -138,3 +138,4 @@ integration: - `Commit statuses`: `Read-only` - `Variables`: `Read & write` (if templates include GitHub Action Repository Variables) - `Secrets`: `Read & write` (if templates include GitHub Action Repository Secrets) + - `Environments`: `Read & write` (if templates include GitHub Environments) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 5e18ad15bf..86fadbacb0 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -211,6 +211,50 @@ export function createGithubActionsDispatchAction(options: { JsonObject >; +// @public +export function createGithubDeployKeyAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction_2< + { + repoUrl: string; + publicKey: string; + privateKey: string; + deployKeyName: string; + privateKeySecretName?: string | undefined; + token?: string | undefined; + }, + JsonObject +>; + +// @public +export function createGithubEnvironmentAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction_2< + { + repoUrl: string; + name: string; + deploymentBranchPolicy?: + | { + protected_branches: boolean; + custom_branch_policies: boolean; + } + | undefined; + customBranchPolicyNames?: string[] | undefined; + environmentVariables?: + | { + [key: string]: string; + } + | undefined; + secrets?: + | { + [key: string]: string; + } + | undefined; + token?: string | undefined; + }, + JsonObject +>; + // @public export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 747b5cdaed..f5ea23f26f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -42,6 +42,8 @@ import { } from './filesystem'; import { createGithubActionsDispatchAction, + createGithubDeployKeyAction, + createGithubEnvironmentAction, createGithubIssuesLabelAction, createGithubRepoCreateAction, createGithubRepoPushAction, @@ -194,6 +196,12 @@ export const createBuiltinActions = ( config, githubCredentialsProvider, }), + createGithubEnvironmentAction({ + integrations, + }), + createGithubDeployKeyAction({ + integrations, + }), ]; return actions as TemplateAction[]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts new file mode 100644 index 0000000000..4df9294ff5 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createGithubDeployKeyAction } from './githubDeployKey'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; + +const mockOctokit = { + rest: { + actions: { + getRepoPublicKey: jest.fn(), + createOrUpdateRepoSecret: jest.fn(), + }, + repos: { + createDeployKey: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +describe('github:deployKey:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + publicKey: 'pubkey', + privateKey: 'privkey', + deployKeyName: 'Push Tags', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + + action = createGithubDeployKeyAction({ + integrations, + }); + }); + + it('should work happy path', async () => { + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + + await action.handler(mockContext); + + expect(mockOctokit.rest.repos.createDeployKey).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + title: 'Push Tags', + key: 'pubkey', + }); + + expect( + mockOctokit.rest.actions.createOrUpdateRepoSecret, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + secret_name: 'PUSH_TAGS_PRIVATE_KEY', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'privateKeySecretName', + 'PUSH_TAGS_PRIVATE_KEY', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts new file mode 100644 index 0000000000..6ad646a89c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; +import { Octokit } from 'octokit'; +import Sodium from 'libsodium-wrappers'; + +/** + * Creates an `github:deployKey:create` Scaffolder action that creates a Deploy Key + * + * @public + */ +export function createGithubDeployKeyAction(options: { + integrations: ScmIntegrationRegistry; +}) { + const { integrations } = options; + // For more information on how to define custom actions, see + // https://backstage.io/docs/features/software-templates/writing-custom-actions + return createTemplateAction<{ + repoUrl: string; + publicKey: string; + privateKey: string; + deployKeyName: string; + privateKeySecretName?: string; + token?: string; + }>({ + id: 'github:deployKey:create', + description: 'Creates and stores Deploy Keys', + schema: { + input: { + type: 'object', + required: ['repoUrl', 'publicKey', 'privateKey', 'deployKeyName'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + publicKey: { + title: 'SSH Public Key', + description: `Generated from ssh-keygen. Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'.`, + type: 'string', + }, + privateKey: { + title: 'SSH Private Key', + description: `SSH Private Key generated from ssh-keygen`, + type: 'string', + }, + deployKeyName: { + title: 'Deploy Key Name', + description: `Name of the Deploy Key`, + type: 'string', + }, + privateKeySecretName: { + title: 'Private Key GitHub Secret Name', + description: `Name of the GitHub Secret to store the private key related to the Deploy Key. Defaults to: 'KEY_NAME_PRIVATE_KEY' where 'KEY_NAME' is the name of the Deploy Key`, + type: 'string', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + }, + }, + output: { + type: 'object', + properties: { + privateKeySecretName: { + title: 'The GitHub Action Repo Secret Name for the Private Key', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + publicKey, + privateKey, + deployKeyName, + privateKeySecretName = `${deployKeyName + .split(' ') + .join('_') + .toLocaleUpperCase('en-US')}_PRIVATE_KEY`, + token: providedToken, + } = ctx.input; + + const octokitOptions = await getOctokitOptions({ + integrations, + token: providedToken, + repoUrl: repoUrl, + }); + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + + const client = new Octokit(octokitOptions); + + await client.rest.repos.createDeployKey({ + owner: owner, + repo: repo, + title: deployKeyName, + key: publicKey, + }); + const publicKeyResponse = await client.rest.actions.getRepoPublicKey({ + owner: owner, + repo: repo, + }); + + await Sodium.ready; + const binaryKey = Sodium.from_base64( + publicKeyResponse.data.key, + Sodium.base64_variants.ORIGINAL, + ); + const binarySecret = Sodium.from_string(privateKey); + const encryptedBinarySecret = Sodium.crypto_box_seal( + binarySecret, + binaryKey, + ); + const encryptedBase64Secret = Sodium.to_base64( + encryptedBinarySecret, + Sodium.base64_variants.ORIGINAL, + ); + + await client.rest.actions.createOrUpdateRepoSecret({ + owner: owner, + repo: repo, + secret_name: privateKeySecretName, + encrypted_value: encryptedBase64Secret, + key_id: publicKeyResponse.data.key_id, + }); + + ctx.output('privateKeySecretName', privateKeySecretName); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts new file mode 100644 index 0000000000..d158bc122e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts @@ -0,0 +1,258 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createGithubEnvironmentAction } from './githubEnvironment'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; + +const mockOctokit = { + rest: { + actions: { + getRepoPublicKey: jest.fn(), + createEnvironmentVariable: jest.fn(), + createOrUpdateEnvironmentSecret: jest.fn(), + }, + repos: { + createDeploymentBranchPolicy: jest.fn(), + createOrUpdateEnvironment: jest.fn(), + get: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +describe('github:environment:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + id: 'repoid', + }, + }); + + action = createGithubEnvironmentAction({ + integrations, + }); + }); + + afterEach(jest.resetAllMocks); + + it('should work happy path', async () => { + await action.handler(mockContext); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + }); + + it('should work specify deploymentBranchPolicy protected', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + deploymentBranchPolicy: { + protected_branches: true, + custom_branch_policies: false, + }, + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: true, + custom_branch_policies: false, + }, + }); + }); + it('should work specify deploymentBranchPolicy custom', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customBranchPolicyNames: ['main', '*.*.*'], + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: false, + custom_branch_policies: true, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: 'main', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: '*.*.*', + }); + }); + + it('should work specify environment variables', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + environmentVariables: { + key1: 'val1', + key2: 'val2', + }, + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + name: 'key1', + value: 'val1', + }); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + name: 'key2', + value: 'val2', + }); + }); + + it('should work specify secrets', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + secrets: { + key1: 'val1', + key2: 'val2', + }, + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + secret_name: 'key1', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + secret_name: 'key2', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts new file mode 100644 index 0000000000..04b33ed259 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts @@ -0,0 +1,200 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; +import { Octokit } from 'octokit'; +import Sodium from 'libsodium-wrappers'; + +/** + * Creates an `github:environment:create` Scaffolder action that creates a Github Environment. + * + * @public + */ +export function createGithubEnvironmentAction(options: { + integrations: ScmIntegrationRegistry; +}) { + const { integrations } = options; + // For more information on how to define custom actions, see + // https://backstage.io/docs/features/software-templates/writing-custom-actions + return createTemplateAction<{ + repoUrl: string; + name: string; + deploymentBranchPolicy?: { + protected_branches: boolean; + custom_branch_policies: boolean; + }; + customBranchPolicyNames?: string[]; + environmentVariables?: { [key: string]: string }; + secrets?: { [key: string]: string }; + token?: string; + }>({ + id: 'github:environment:create', + description: 'Creates Deployment Environments', + schema: { + input: { + type: 'object', + required: ['repoUrl', 'name'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + name: { + title: 'Environment Name', + description: `Name of the deployment environment to create`, + type: 'string', + }, + deploymentBranchPolicy: { + title: 'Deployment Branch Policy', + description: `The type of deployment branch policy for this environment. To allow all branches to deploy, set to null.`, + type: 'object', + required: ['protected_branches', 'custom_branch_policies'], + properties: { + protected_branches: { + title: 'Protected Branches', + description: `Whether only branches with branch protection rules can deploy to this environment. If protected_branches is true, custom_branch_policies must be false; if protected_branches is false, custom_branch_policies must be true.`, + type: 'boolean', + }, + custom_branch_policies: { + title: 'Custom Branch Policies', + description: `Whether only branches that match the specified name patterns can deploy to this environment. If custom_branch_policies is true, protected_branches must be false; if custom_branch_policies is false, protected_branches must be true.`, + type: 'boolean', + }, + }, + }, + customBranchPolicyNames: { + title: 'Custom Branch Policy Name', + description: `The name pattern that branches must match in order to deploy to the environment. + + Wildcard characters will not match /. For example, to match branches that begin with release/ and contain an additional single slash, use release/*/*. For more information about pattern matching syntax, see the Ruby File.fnmatch documentation.`, + type: 'array', + items: { + type: 'string', + }, + }, + environmentVariables: { + title: 'Environment Variables', + description: `Environment variables attached to the deployment environment`, + type: 'object', + }, + secrets: { + title: 'Deployment Secrets', + description: `Secrets attached to the deployment environment`, + type: 'object', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + name, + deploymentBranchPolicy, + customBranchPolicyNames, + environmentVariables, + secrets, + token: providedToken, + } = ctx.input; + + const octokitOptions = await getOctokitOptions({ + integrations, + token: providedToken, + repoUrl: repoUrl, + }); + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + + const client = new Octokit(octokitOptions); + const repository = await client.rest.repos.get({ + owner: owner, + repo: repo, + }); + + await client.rest.repos.createOrUpdateEnvironment({ + owner: owner, + repo: repo, + environment_name: name, + deployment_branch_policy: deploymentBranchPolicy ?? null, + }); + + if (customBranchPolicyNames) { + for (const item of customBranchPolicyNames) { + await client.rest.repos.createDeploymentBranchPolicy({ + owner: owner, + repo: repo, + environment_name: name, + name: item, + }); + } + } + + for (const [key, value] of Object.entries(environmentVariables ?? {})) { + await client.rest.actions.createEnvironmentVariable({ + repository_id: repository.data.id, + environment_name: name, + name: key, + value, + }); + } + + if (secrets) { + const publicKeyResponse = await client.rest.actions.getRepoPublicKey({ + owner: owner, + repo: repo, + }); + + await Sodium.ready; + const binaryKey = Sodium.from_base64( + publicKeyResponse.data.key, + Sodium.base64_variants.ORIGINAL, + ); + for (const [key, value] of Object.entries(secrets)) { + const binarySecret = Sodium.from_string(value); + const encryptedBinarySecret = Sodium.crypto_box_seal( + binarySecret, + binaryKey, + ); + const encryptedBase64Secret = Sodium.to_base64( + encryptedBinarySecret, + Sodium.base64_variants.ORIGINAL, + ); + + await client.rest.actions.createOrUpdateEnvironmentSecret({ + repository_id: repository.data.id, + environment_name: name, + secret_name: key, + encrypted_value: encryptedBase64Secret, + key_id: publicKeyResponse.data.key_id, + }); + } + } + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index dc1b4eb91c..edef7cec5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -98,7 +98,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( client: Octokit, repo: string, owner: string, - repoVisibility: 'private' | 'internal' | 'public', + repoVisibility: 'private' | 'internal' | 'public' | undefined, description: string | undefined, homepage: string | undefined, deleteBranchOnMerge: boolean, @@ -149,7 +149,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( name: repo, org: owner, private: repoVisibility === 'private', - // @ts-ignore + // @ts-ignore https://github.com/octokit/types.ts/issues/522 visibility: repoVisibility, description: description, delete_branch_on_merge: deleteBranchOnMerge, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index 6746aef37e..ecc8ce59f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -19,3 +19,5 @@ export { createGithubIssuesLabelAction } from './githubIssuesLabel'; export { createGithubRepoCreateAction } from './githubRepoCreate'; export { createGithubRepoPushAction } from './githubRepoPush'; export { createGithubWebhookAction } from './githubWebhook'; +export { createGithubDeployKeyAction } from './githubDeployKey'; +export { createGithubEnvironmentAction } from './githubEnvironment'; From c5fe9177fa72acd514c44911a0e798c2321f7677 Mon Sep 17 00:00:00 2001 From: djamaile Date: Fri, 2 Jun 2023 17:52:33 +0200 Subject: [PATCH 24/78] Add blog about backstage deploy Signed-off-by: djamaile --- .../2023-05-06-backstage-deploy-alpha.mdx | 77 ++++++++++++++++++ .../2023-05-06/Welcome-Backstage-Deploy.png | Bin 0 -> 122242 bytes 2 files changed, 77 insertions(+) create mode 100644 microsite/blog/2023-05-06-backstage-deploy-alpha.mdx create mode 100644 microsite/blog/assets/2023-05-06/Welcome-Backstage-Deploy.png diff --git a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx new file mode 100644 index 0000000000..ead9eb5f29 --- /dev/null +++ b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx @@ -0,0 +1,77 @@ +--- +title: "Welcome: Backstage Deploy CLI (Alpha)" +author: Djamaile Rahamat, Spotify +authorURL: https://github.com/djamaile +authorImageURL: https://avatars.githubusercontent.com/u/15789670?v=4 +--- + +_This blog post is co-authored by Emma White ([@ewhite1997](https://github.com/ewhite1997)), Spotify._ + +![Backstage Deploy banner](assets/2023-05-06/Welcome-Backstage-Deploy.png) + +TL;DR: Backstage released a new CLI earlier this year as part of a [new Backstage project](https://github.com/backstage/backstage-deploy). This new capability allows early adopters to deploy their Backstage proof of concept faster than before. In this blog, you’ll learn a little bit more about the Deploy CLI and how you can get started with it. + +{/* truncate */} + + +## Why Backstage Deploy + +We’ve heard from Backstage adopters that before reaching widespread adoption at their organizations, teams test out a Backstage proof of concept (POC). To create the POC, teams scaffold an instance, set up the catalog, and change the UI to suit their company’s branding. Once these milestones are completed, there's just one last step before showing off the awesomeness of a Backstage POC internally... deploying the Backstage POC from a local machine to the cloud. This final step often introduces a lot of questions that attempt to determine the easiest and fastest way to do that. + +Let’s say you’re at this last step: your POC is ready for deployment, but you have some lingering questions. + +For example: +- What Dockerfile should you use? +- Should the POC be deployed on Kubernetes? +- How do you deploy the POC on my cloud provider? + +To address these questions and reduce friction from deploying a POC, we built a [new CLI](https://github.com/backstage/backstage-deploy). The CLI is simply called `deploy` and is invokable with `npx`. With this new CLI, you can generate a Dockerfile and deploy a Backstage instance onto a preferred cloud provider. While the package’s infrastructure is built to support all cloud providers, currently the CLI only offers an AWS implementation. In the future, we plan to add additional cloud providers to the package and [welcome any contributions](https://github.com/backstage/backstage/blob/6996b3338d678efc03307112524060e9dc2ad769/CONTRIBUTING.md) extending the suite of cloud provider implementations! + +**So, what about Kubernetes?** Since the CLI is designed specifically for the POC phase, we believe that Kubernetes isn’t the right fit – as Kubernetes is better suited for production workloads. So, we explored lightweight, container-based solutions, and landed on [Amazon Lightsail](https://docs.aws.amazon.com/lightsail/index.html) as a hosting service for the POC. +Amazon Lightsail supports [lightweight container](https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-container-services) deployments [at a low cost](https://aws.amazon.com/lightsail/pricing/), and has a [free trial for new users](https://aws.amazon.com/lightsail/pricing/?loc=ft#AWS_Free_Tier)! Given Lightsail’s ease of use and affordability, we recommend using Lightsail over Kubernetes to test out your Backstage POC. + +With the Dockerfile and Kubernetes questions answered, we just have to address how to actually deploy the POC. And that is where the new CLI comes in. + +## Get started with Deploy CLI + +It's now time to put the Deploy CLI into action and deploy a Backstage POC. The following steps guide you through the deployment process. + +Deploy CLI makes use of [Pulumi](https://www.pulumi.com/docs/) which is an infrastructure-as-code tool that helps provision resources in the cloud. Before you get started, ensure you have the [Pulumi CLI](https://www.pulumi.com/docs/cli/) and [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed on your local machine. + +You can find the step-by-step installation documentation for Pulumi under the [Deployment section](https://backstage.io/docs/deployment/pulumi/aws). + +But we summarize the steps to deploy your POC here. You can invoke the the CLI you can use the following command: + +```bash +$ npx backstage-deploy aws --create-dockerfile +Starting Pulumi +successfully initialized stack +installing aws plugin... +plugins installed +setting up config +refreshing stack... +Refreshing (backstage): + +Instance will live at: +https://backstage-container-service.xxx.us-east-1.cs.amazonlightsail.com/ +``` + +Once you kick off the command it will create a Dockerfile for you in the root and Pulumi will start provisioning resources in AWS. +Pulumi will then create a container based deployment in Amazon Lightsail. + +After you’re done with the POC, it’s easy to delete the AWS resources you provisioned and save you costs within AWS. You can do so with the following command: + +```bash +$ npx backstage-deploy aws --destroy +``` +## Looking ahead + +As mentioned earlier in this blog, we released the Backstage Deploy CLI with an AWS implementation only. In the next few months, we are excited to collect feedback from our end users to understand how the AWS implementation works. With those learnings, we plan to iterate on the Deploy CLI and build out implementations for other cloud providers. Of course, contributions are always welcome! + +So, please try this new CLI out and let us know what you think on [Discord](https://discord.com/channels/687207715902193673/995973463208644678)! + +Useful links: + +- [Deploy docs](https://backstage.io/docs/deployment/pulumi/aws) +- [Pulumi docs](https://www.pulumi.com/docs/) +- [#deployment](https://discord.com/channels/687207715902193673/995973463208644678) channel in Discord diff --git a/microsite/blog/assets/2023-05-06/Welcome-Backstage-Deploy.png b/microsite/blog/assets/2023-05-06/Welcome-Backstage-Deploy.png new file mode 100644 index 0000000000000000000000000000000000000000..403f12f2bba4c637b91badff692c0ec42658530f GIT binary patch literal 122242 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 Date: Mon, 5 Jun 2023 15:08:19 +0800 Subject: [PATCH 25/78] chore: Add `TargetBranchName` variable and output for the `publish:gitlab:merge-request` and `publish:github:pull-request` s'cascaffolder actions. Signed-off-by: mingfu --- .changeset/fifty-shirts-brush.md | 5 + plugins/scaffolder-backend/api-report.md | 5 + .../builtin/publish/githubPullRequest.test.ts | 92 +++++++++++++++++++ .../builtin/publish/githubPullRequest.ts | 24 ++++- .../publish/gitlabMergeRequest.test.ts | 51 ++++++++++ .../builtin/publish/gitlabMergeRequest.ts | 28 ++++-- 6 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 .changeset/fifty-shirts-brush.md diff --git a/.changeset/fifty-shirts-brush.md b/.changeset/fifty-shirts-brush.md new file mode 100644 index 0000000000..98cd7b885c --- /dev/null +++ b/.changeset/fifty-shirts-brush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add `TargetBranchName` variable and output for the `publish:gitlab:merge-request` and `publish:github:pull-request` s'cascaffolder actions. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 5e18ad15bf..0e686c31f3 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -584,6 +584,7 @@ export const createPublishGithubPullRequestAction: ( { title: string; branchName: string; + targetBranchName?: string | undefined; description: string; repoUrl: string; draft?: boolean | undefined; @@ -626,6 +627,7 @@ export const createPublishGitlabMergeRequestAction: (options: { title: string; description: string; branchName: string; + targetBranchName?: string | undefined; sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; @@ -772,6 +774,9 @@ export type OctokitWithPullRequestPluginClient = Octokit & { data: { html_url: string; number: number; + base: { + ref: string; + }; }; } | null>; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index 7750277728..82f8d22aea 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -62,6 +62,9 @@ describe('createPublishGithubPullRequestAction', () => { data: { html_url: 'https://github.com/myorg/myrepo/pull/123', number: 123, + base: { + ref: 'main', + }, }, }; }), @@ -90,6 +93,94 @@ describe('createPublishGithubPullRequestAction', () => { jest.resetAllMocks(); }); + describe('with targetBranchName', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + fakeClient = { + createPullRequest: jest.fn(async (_: any) => { + return { + url: 'https://api.github.com/myorg/myrepo/pull/123', + headers: {}, + status: 201, + data: { + html_url: 'https://github.com/myorg/myrepo/pull/123', + number: 123, + base: { + ref: 'test', + }, + }, + }; + }), + rest: { + pulls: { + requestReviewers: jest.fn(async (_: any) => ({ data: {} })), + }, + }, + }; + + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + targetBranchName: 'test', + description: 'This PR is really good', + draft: true, + }; + + mockFs({ + [workspacePath]: { 'file.txt': 'Hello there!' }, + }); + + ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + }); + + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + base: 'test', + body: 'This PR is really good', + draft: true, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + }); + + it('creates outputs for the pull request url and number', async () => { + await instance.handler(ctx); + + expect(ctx.output).toHaveBeenCalledWith('targetBranchName', 'test'); + expect(ctx.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + }); + describe('with no sourcePath', () => { let input: GithubPullRequestActionInput; let ctx: ActionContext; @@ -145,6 +236,7 @@ describe('createPublishGithubPullRequestAction', () => { it('creates outputs for the pull request url and number', async () => { await instance.handler(ctx); + expect(ctx.output).toHaveBeenCalledWith('targetBranchName', 'main'); expect(ctx.output).toHaveBeenCalledWith( 'remoteUrl', 'https://github.com/myorg/myrepo/pull/123', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 6706ad9c06..167fedc88e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -42,6 +42,9 @@ export type OctokitWithPullRequestPluginClient = Octokit & { data: { html_url: string; number: number; + base: { + ref: string; + }; }; } | null>; }; @@ -128,6 +131,7 @@ export const createPublishGithubPullRequestAction = ( return createTemplateAction<{ title: string; branchName: string; + targetBranchName?: string; description: string; repoUrl: string; draft?: boolean; @@ -154,6 +158,11 @@ export const createPublishGithubPullRequestAction = ( title: 'Branch Name', description: 'The name for the branch', }, + targetBranchName: { + type: 'string', + title: 'Target Branch Name', + description: 'The target branch name of the merge request', + }, title: { type: 'string', title: 'Pull Request Name', @@ -214,6 +223,10 @@ export const createPublishGithubPullRequestAction = ( required: ['remoteUrl'], type: 'object', properties: { + targetBranchName: { + title: 'Target branch name of the merge request', + type: 'string', + }, remoteUrl: { type: 'string', title: 'Pull Request URL', @@ -231,6 +244,7 @@ export const createPublishGithubPullRequestAction = ( const { repoUrl, branchName, + targetBranchName, title, description, draft, @@ -298,7 +312,7 @@ export const createPublishGithubPullRequestAction = ( ); try { - const response = await client.createPullRequest({ + const createOptions: createPullRequest.Options = { owner, repo, title, @@ -311,7 +325,11 @@ export const createPublishGithubPullRequestAction = ( body: description, head: branchName, draft, - }); + }; + if (targetBranchName) { + createOptions.base = targetBranchName; + } + const response = await client.createPullRequest(createOptions); if (!response) { throw new GithubResponseError('null response from Github'); @@ -329,6 +347,8 @@ export const createPublishGithubPullRequestAction = ( ); } + const targetBranch = response.data.base.ref; + ctx.output('targetBranchName', targetBranch); ctx.output('remoteUrl', response.data.html_url); ctx.output('pullRequestNumber', pullRequestNumber); } catch (e) { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts index c3d0fe58f1..afb93b348a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -101,6 +101,49 @@ describe('createGitLabMergeRequest', () => { mockFs.restore(); }); + describe('createGitLabMergeRequestWithSpecifiedTargetBranch', () => { + it('removeSourceBranch is false by default when not passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + targetBranchName: 'test', + description: 'This MR is really good', + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.Projects.show).not.toHaveBeenCalled(); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'test', + ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'test', + 'Create my new MR', + { description: 'This MR is really good', removeSourceBranch: false }, + ); + expect(ctx.output).toHaveBeenCalledWith('targetBranchName', 'test'); + }); + }); + describe('createGitLabMergeRequestWithoutRemoveBranch', () => { it('removeSourceBranch is false by default when not passed in options', async () => { const input = { @@ -126,6 +169,12 @@ describe('createGitLabMergeRequest', () => { }; await instance.handler(ctx); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith('owner/repo'); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -133,6 +182,8 @@ describe('createGitLabMergeRequest', () => { 'Create my new MR', { description: 'This MR is really good', removeSourceBranch: false }, ); + + expect(ctx.output).toHaveBeenCalledWith('targetBranchName', 'main'); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 3dfe0cff5c..8abb53288c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -39,6 +39,7 @@ export const createPublishGitlabMergeRequestAction = (options: { title: string; description: string; branchName: string; + targetBranchName?: string; sourcePath?: string; targetPath?: string; token?: string; @@ -77,8 +78,13 @@ export const createPublishGitlabMergeRequestAction = (options: { }, branchName: { type: 'string', - title: 'Destination Branch name', - description: 'The description of the merge request', + title: 'Source Branch Name', + description: 'The source branch name of the merge request', + }, + targetBranchName: { + type: 'string', + title: 'Target Branch Name', + description: 'The target branch name of the merge request', }, sourcePath: { type: 'string', @@ -119,6 +125,10 @@ export const createPublishGitlabMergeRequestAction = (options: { output: { type: 'object', properties: { + targetBranchName: { + title: 'Target branch name of the merge request', + type: 'string', + }, projectid: { title: 'Gitlab Project id/Name(slug)', type: 'string', @@ -139,6 +149,7 @@ export const createPublishGitlabMergeRequestAction = (options: { const { assignee, branchName, + targetBranchName, description, repoUrl, removeSourceBranch, @@ -211,12 +222,16 @@ export const createPublishGitlabMergeRequestAction = (options: { execute_filemode: file.executable, })); - const projects = await api.Projects.show(repoID); + let targetBranch = targetBranchName; + if (!targetBranch) { + const projects = await api.Projects.show(repoID); - const { default_branch: defaultBranch } = projects; + const { default_branch: defaultBranch } = projects; + targetBranch = defaultBranch!; + } try { - await api.Branches.create(repoID, branchName, String(defaultBranch)); + await api.Branches.create(repoID, branchName, String(targetBranch)); } catch (e) { throw new InputError( `The branch creation failed. Please check that your repo does not already contain a branch named '${branchName}'. ${e}`, @@ -235,7 +250,7 @@ export const createPublishGitlabMergeRequestAction = (options: { const mergeRequestUrl = await api.MergeRequests.create( repoID, branchName, - String(defaultBranch), + String(targetBranch), title, { description, @@ -246,6 +261,7 @@ export const createPublishGitlabMergeRequestAction = (options: { return mergeRequest.web_url; }); ctx.output('projectid', repoID); + ctx.output('targetBranchName', targetBranch); ctx.output('projectPath', repoID); ctx.output('mergeRequestUrl', mergeRequestUrl); } catch (e) { From 229b3efcbcd9884fad70c05eeb24ed03d8e81564 Mon Sep 17 00:00:00 2001 From: Djam Date: Mon, 5 Jun 2023 10:44:18 +0200 Subject: [PATCH 26/78] Update 2023-05-06-backstage-deploy-alpha.mdx Signed-off-by: Djam --- microsite/blog/2023-05-06-backstage-deploy-alpha.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx index ead9eb5f29..422ba63f3b 100644 --- a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx +++ b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx @@ -72,6 +72,6 @@ So, please try this new CLI out and let us know what you think on [Discord](http Useful links: -- [Deploy docs](https://backstage.io/docs/deployment/pulumi/aws) +- [Deploy docs](https://backstage.io/docs/deployment/pulumi/aws-lightsail) - [Pulumi docs](https://www.pulumi.com/docs/) - [#deployment](https://discord.com/channels/687207715902193673/995973463208644678) channel in Discord From 4c49f403a7cfe16b6d0f4dd4873a143559e495e0 Mon Sep 17 00:00:00 2001 From: djamaile Date: Mon, 5 Jun 2023 12:14:47 +0200 Subject: [PATCH 27/78] chore: run prettier Signed-off-by: djamaile --- .../2023-05-06-backstage-deploy-alpha.mdx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx index 422ba63f3b..5c4c3d2164 100644 --- a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx +++ b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx @@ -1,5 +1,5 @@ --- -title: "Welcome: Backstage Deploy CLI (Alpha)" +title: 'Welcome: Backstage Deploy CLI (Alpha)' author: Djamaile Rahamat, Spotify authorURL: https://github.com/djamaile authorImageURL: https://avatars.githubusercontent.com/u/15789670?v=4 @@ -9,36 +9,36 @@ _This blog post is co-authored by Emma White ([@ewhite1997](https://github.com/e ![Backstage Deploy banner](assets/2023-05-06/Welcome-Backstage-Deploy.png) -TL;DR: Backstage released a new CLI earlier this year as part of a [new Backstage project](https://github.com/backstage/backstage-deploy). This new capability allows early adopters to deploy their Backstage proof of concept faster than before. In this blog, you’ll learn a little bit more about the Deploy CLI and how you can get started with it. +TL;DR: Backstage released a new CLI earlier this year as part of a [new Backstage project](https://github.com/backstage/backstage-deploy). This new capability allows early adopters to deploy their Backstage proof of concept faster than before. In this blog, you’ll learn a little bit more about the Deploy CLI and how you can get started with it. {/* truncate */} +## Why Backstage Deploy -## Why Backstage Deploy +We’ve heard from Backstage adopters that before reaching widespread adoption at their organizations, teams test out a Backstage proof of concept (POC). To create the POC, teams scaffold an instance, set up the catalog, and change the UI to suit their company’s branding. Once these milestones are completed, there's just one last step before showing off the awesomeness of a Backstage POC internally... deploying the Backstage POC from a local machine to the cloud. This final step often introduces a lot of questions that attempt to determine the easiest and fastest way to do that. -We’ve heard from Backstage adopters that before reaching widespread adoption at their organizations, teams test out a Backstage proof of concept (POC). To create the POC, teams scaffold an instance, set up the catalog, and change the UI to suit their company’s branding. Once these milestones are completed, there's just one last step before showing off the awesomeness of a Backstage POC internally... deploying the Backstage POC from a local machine to the cloud. This final step often introduces a lot of questions that attempt to determine the easiest and fastest way to do that. - -Let’s say you’re at this last step: your POC is ready for deployment, but you have some lingering questions. +Let’s say you’re at this last step: your POC is ready for deployment, but you have some lingering questions. For example: + - What Dockerfile should you use? - Should the POC be deployed on Kubernetes? -- How do you deploy the POC on my cloud provider? +- How do you deploy the POC on my cloud provider? -To address these questions and reduce friction from deploying a POC, we built a [new CLI](https://github.com/backstage/backstage-deploy). The CLI is simply called `deploy` and is invokable with `npx`. With this new CLI, you can generate a Dockerfile and deploy a Backstage instance onto a preferred cloud provider. While the package’s infrastructure is built to support all cloud providers, currently the CLI only offers an AWS implementation. In the future, we plan to add additional cloud providers to the package and [welcome any contributions](https://github.com/backstage/backstage/blob/6996b3338d678efc03307112524060e9dc2ad769/CONTRIBUTING.md) extending the suite of cloud provider implementations! +To address these questions and reduce friction from deploying a POC, we built a [new CLI](https://github.com/backstage/backstage-deploy). The CLI is simply called `deploy` and is invokable with `npx`. With this new CLI, you can generate a Dockerfile and deploy a Backstage instance onto a preferred cloud provider. While the package’s infrastructure is built to support all cloud providers, currently the CLI only offers an AWS implementation. In the future, we plan to add additional cloud providers to the package and [welcome any contributions](https://github.com/backstage/backstage/blob/6996b3338d678efc03307112524060e9dc2ad769/CONTRIBUTING.md) extending the suite of cloud provider implementations! **So, what about Kubernetes?** Since the CLI is designed specifically for the POC phase, we believe that Kubernetes isn’t the right fit – as Kubernetes is better suited for production workloads. So, we explored lightweight, container-based solutions, and landed on [Amazon Lightsail](https://docs.aws.amazon.com/lightsail/index.html) as a hosting service for the POC. -Amazon Lightsail supports [lightweight container](https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-container-services) deployments [at a low cost](https://aws.amazon.com/lightsail/pricing/), and has a [free trial for new users](https://aws.amazon.com/lightsail/pricing/?loc=ft#AWS_Free_Tier)! Given Lightsail’s ease of use and affordability, we recommend using Lightsail over Kubernetes to test out your Backstage POC. +Amazon Lightsail supports [lightweight container](https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-container-services) deployments [at a low cost](https://aws.amazon.com/lightsail/pricing/), and has a [free trial for new users](https://aws.amazon.com/lightsail/pricing/?loc=ft#AWS_Free_Tier)! Given Lightsail’s ease of use and affordability, we recommend using Lightsail over Kubernetes to test out your Backstage POC. -With the Dockerfile and Kubernetes questions answered, we just have to address how to actually deploy the POC. And that is where the new CLI comes in. +With the Dockerfile and Kubernetes questions answered, we just have to address how to actually deploy the POC. And that is where the new CLI comes in. ## Get started with Deploy CLI It's now time to put the Deploy CLI into action and deploy a Backstage POC. The following steps guide you through the deployment process. -Deploy CLI makes use of [Pulumi](https://www.pulumi.com/docs/) which is an infrastructure-as-code tool that helps provision resources in the cloud. Before you get started, ensure you have the [Pulumi CLI](https://www.pulumi.com/docs/cli/) and [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed on your local machine. +Deploy CLI makes use of [Pulumi](https://www.pulumi.com/docs/) which is an infrastructure-as-code tool that helps provision resources in the cloud. Before you get started, ensure you have the [Pulumi CLI](https://www.pulumi.com/docs/cli/) and [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed on your local machine. -You can find the step-by-step installation documentation for Pulumi under the [Deployment section](https://backstage.io/docs/deployment/pulumi/aws). +You can find the step-by-step installation documentation for Pulumi under the [Deployment section](https://backstage.io/docs/deployment/pulumi/aws). But we summarize the steps to deploy your POC here. You can invoke the the CLI you can use the following command: @@ -59,14 +59,15 @@ https://backstage-container-service.xxx.us-east-1.cs.amazonlightsail.com/ Once you kick off the command it will create a Dockerfile for you in the root and Pulumi will start provisioning resources in AWS. Pulumi will then create a container based deployment in Amazon Lightsail. -After you’re done with the POC, it’s easy to delete the AWS resources you provisioned and save you costs within AWS. You can do so with the following command: +After you’re done with the POC, it’s easy to delete the AWS resources you provisioned and save you costs within AWS. You can do so with the following command: ```bash $ npx backstage-deploy aws --destroy ``` + ## Looking ahead -As mentioned earlier in this blog, we released the Backstage Deploy CLI with an AWS implementation only. In the next few months, we are excited to collect feedback from our end users to understand how the AWS implementation works. With those learnings, we plan to iterate on the Deploy CLI and build out implementations for other cloud providers. Of course, contributions are always welcome! +As mentioned earlier in this blog, we released the Backstage Deploy CLI with an AWS implementation only. In the next few months, we are excited to collect feedback from our end users to understand how the AWS implementation works. With those learnings, we plan to iterate on the Deploy CLI and build out implementations for other cloud providers. Of course, contributions are always welcome! So, please try this new CLI out and let us know what you think on [Discord](https://discord.com/channels/687207715902193673/995973463208644678)! From 7b8abaf5a8695787118cf08afffbaf254f6d0974 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:08:50 +0000 Subject: [PATCH 28/78] fix(deps): update dependency @tanstack/react-query to v4.29.12 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 68c2699de5..2a25a00335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15778,18 +15778,18 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:4.27.0": - version: 4.27.0 - resolution: "@tanstack/query-core@npm:4.27.0" - checksum: 914b4f01ac75632f4603b650e9105f64c739b79250baf76aad16cee216f7bad6060c04eda0c70838fe61a5f6c17883e19fcc3075b2433a906539da95ce785796 +"@tanstack/query-core@npm:4.29.11": + version: 4.29.11 + resolution: "@tanstack/query-core@npm:4.29.11" + checksum: 2a17223f34e99e17d564a15206668eb79324da72e630a47cf41690f92fb091a8669bdc89e9ab1b0d8f0e3e8f579529cfb0aa6f10b4fc58ea81cc7e5caede9229 languageName: node linkType: hard "@tanstack/react-query@npm:^4.1.3": - version: 4.28.0 - resolution: "@tanstack/react-query@npm:4.28.0" + version: 4.29.12 + resolution: "@tanstack/react-query@npm:4.29.12" dependencies: - "@tanstack/query-core": 4.27.0 + "@tanstack/query-core": 4.29.11 use-sync-external-store: ^1.2.0 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -15800,7 +15800,7 @@ __metadata: optional: true react-native: optional: true - checksum: 7baa70d22b55cfa22f8a1d92ba9f8379c5436ee81fce3f0e7df4a8c29bb26bfef4a83759cf10f752b40ae2940099fcce88ce91a773240f4999ae91136980c6e3 + checksum: aea231af648688c5eafdbbda0667034221cca8b77e00690ab54950b7cc0296d14646931aee44fc5f18d5ce4d683b5d234b053eed5c0a5c340bbbacdab873a70b languageName: node linkType: hard From 8f6b60bca12040f77e1881260f5b42787d3393df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:09:22 +0000 Subject: [PATCH 29/78] fix(deps): update dependency @uiw/react-codemirror to v4.21.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 68c2699de5..1e15151df4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18181,9 +18181,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.19.16": - version: 4.19.16 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.16" +"@uiw/codemirror-extensions-basic-setup@npm:4.21.0": + version: 4.21.0 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.0" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -18200,19 +18200,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: e5263f19e1ca838ac9cc7c0ab03d4d1ca2453ff306e17d354b0772954c14cfaa146b0a00ed2d5690f5687aeafdd5f2f5587efc573ffb5714bd77f6af46444f74 + checksum: 9b39f219cbbe00653a295ad05b6125c5b960f0343dfd68e1d9e0f97d4fba023bccb43915647ac3a9cbbb9e9ff54ffb9addfe3114aa22038952e1e1e59e3ab418 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.19.16 - resolution: "@uiw/react-codemirror@npm:4.19.16" + version: 4.21.0 + resolution: "@uiw/react-codemirror@npm:4.21.0" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.19.16 + "@uiw/codemirror-extensions-basic-setup": 4.21.0 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -18222,7 +18222,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 0625a544eb53ff1236e75dc4c81400285bf4dfdacd5c9c71129066f7decb7e902c97f756c6edc854d44b586ad4b0d3d303df065429532df9085c5f6b1adbad80 + checksum: 783a9b4fd3fc655d8a15fd7847ad36a3eb65cb88ac268622adf9cc93e55bc42a4f5a59cf288fc6ef05763dc090b7f5d21468633a7ead519a89a50d8db7bd4c8f languageName: node linkType: hard From 3fbeeae1bbc1baf785d73668da522f36afd3f834 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:11:47 +0000 Subject: [PATCH 30/78] fix(deps): update dependency core-js to v3.30.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc84346c1a..688f5de26a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21629,9 +21629,9 @@ __metadata: linkType: hard "core-js@npm:^3.6.5": - version: 3.29.1 - resolution: "core-js@npm:3.29.1" - checksum: b38446dbfcfd3887b3d4922990da487e2c95044cb4c5717aaf95e786a4c6b218f05c056c7ed6c699169b9794a49fec890e402659d54661fc56965a0eb717e7bd + version: 3.30.2 + resolution: "core-js@npm:3.30.2" + checksum: 73d47e2b9d9f502800973982d08e995bbf04832e20b04e04be31dd7607247158271315e9328788a2408190e291c7ffbefad141167b1e57dea9f983e1e723541e languageName: node linkType: hard From 141b7d3fe78faf6046040ae665965765d48ea248 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:12:20 +0000 Subject: [PATCH 31/78] fix(deps): update dependency cronstrue to v2.27.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc84346c1a..26538bcc8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21783,11 +21783,11 @@ __metadata: linkType: hard "cronstrue@npm:^2.2.0": - version: 2.24.0 - resolution: "cronstrue@npm:2.24.0" + version: 2.27.0 + resolution: "cronstrue@npm:2.27.0" bin: cronstrue: bin/cli.js - checksum: 2c7d4cfe43bec7b57b1824772641271bb5f5ed0209336ea53796f97b17966dd95cf15ae890f7c2494f639ba489d225bfb882950cda0ec7e5453fa7bf236842dd + checksum: 310d933c4448896519c203a434dc1ede7029967df941e810f02bafb62146045c16df4987edde344d757bafb6d45f63c9ecb7385df19b985ca91486f4efa6ff7b languageName: node linkType: hard From e74df57b2cfbd102fde38c16293b402e317a4483 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:12:38 +0000 Subject: [PATCH 32/78] fix(deps): update dependency css-loader to v6.8.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5786798323..97c01b6054 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21924,20 +21924,20 @@ __metadata: linkType: hard "css-loader@npm:^6.5.1": - version: 6.7.4 - resolution: "css-loader@npm:6.7.4" + version: 6.8.1 + resolution: "css-loader@npm:6.8.1" dependencies: icss-utils: ^5.1.0 postcss: ^8.4.21 postcss-modules-extract-imports: ^3.0.0 - postcss-modules-local-by-default: ^4.0.1 + postcss-modules-local-by-default: ^4.0.3 postcss-modules-scope: ^3.0.0 postcss-modules-values: ^4.0.0 postcss-value-parser: ^4.2.0 semver: ^7.3.8 peerDependencies: webpack: ^5.0.0 - checksum: 6021fa9e375d767b9675e295c1513f2ee4ae04f76d9de69a75b8446e05f6e02b2170407ea72939925b788dcd5aa308527f6b41be3870dc1f4b0bfff8d2532c6e + checksum: 7c1784247bdbe76dc5c55fb1ac84f1d4177a74c47259942c9cfdb7a8e6baef11967a0bc85ac285f26bd26d5059decb848af8154a03fdb4f4894f41212f45eef3 languageName: node linkType: hard @@ -34495,16 +34495,16 @@ __metadata: languageName: node linkType: hard -"postcss-modules-local-by-default@npm:^4.0.0, postcss-modules-local-by-default@npm:^4.0.1": - version: 4.0.1 - resolution: "postcss-modules-local-by-default@npm:4.0.1" +"postcss-modules-local-by-default@npm:^4.0.0, postcss-modules-local-by-default@npm:^4.0.3": + version: 4.0.3 + resolution: "postcss-modules-local-by-default@npm:4.0.3" dependencies: icss-utils: ^5.0.0 postcss-selector-parser: ^6.0.2 postcss-value-parser: ^4.1.0 peerDependencies: postcss: ^8.1.0 - checksum: 1a95eb8bc54a363a77dbd77a0a88f500e7937ecbed5903becef9362eace28de406e6fdf62640126c22964678370e87eb10481eea2703702772935b4515603bfd + checksum: 2f8083687f3d6067885f8863dd32dbbb4f779cfcc7e52c17abede9311d84faf6d3ed8760e7c54c6380281732ae1f78e5e56a28baf3c271b33f450a11c9e30485 languageName: node linkType: hard From 243b79bac2e4278fd955854195122c71fafc2a02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:13:11 +0000 Subject: [PATCH 33/78] fix(deps): update dependency elastic-builder to v2.19.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5786798323..5fd85b1da1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23291,8 +23291,8 @@ __metadata: linkType: hard "elastic-builder@npm:^2.16.0": - version: 2.18.1 - resolution: "elastic-builder@npm:2.18.1" + version: 2.19.0 + resolution: "elastic-builder@npm:2.19.0" dependencies: lodash.has: ^4.5.2 lodash.hasin: ^4.5.2 @@ -23302,7 +23302,7 @@ __metadata: lodash.isobject: ^3.0.2 lodash.isstring: ^4.0.1 lodash.omit: ^4.5.0 - checksum: 54093daadc1fc3666ac8eef0a268267761e92a3fbb524e51936cf944ceeaeb231a269fc8ae9fb902ca41c976c76c0c037da692a34577d705da301ed727bd8e07 + checksum: 14c3a2e2a76f44425dfff4c57e16f077553845976422cb35c35d235e1ea59942b86c379e516828e8390e3c53360a564ff94ee3aa961a5f2fb791da7e5e97012c languageName: node linkType: hard From 54c1d95bdd2065a4298987af9276c337cab652b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 15:12:33 +0000 Subject: [PATCH 34/78] fix(deps): update dependency @uiw/react-codemirror to v4.21.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5052893de4..50c2bb2d5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18181,9 +18181,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.21.0": - version: 4.21.0 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.0" +"@uiw/codemirror-extensions-basic-setup@npm:4.21.1": + version: 4.21.1 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.1" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -18200,19 +18200,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 9b39f219cbbe00653a295ad05b6125c5b960f0343dfd68e1d9e0f97d4fba023bccb43915647ac3a9cbbb9e9ff54ffb9addfe3114aa22038952e1e1e59e3ab418 + checksum: ac1f90f9396b5542bd30222c513354e6756972c455931e4a48327ceaadb4c0c9fa0763a87c6d166ee3cfe45941cb348661f6ee4672f71fd67e8708e73231095b languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.21.0 - resolution: "@uiw/react-codemirror@npm:4.21.0" + version: 4.21.1 + resolution: "@uiw/react-codemirror@npm:4.21.1" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.21.0 + "@uiw/codemirror-extensions-basic-setup": 4.21.1 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -18222,7 +18222,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 783a9b4fd3fc655d8a15fd7847ad36a3eb65cb88ac268622adf9cc93e55bc42a4f5a59cf288fc6ef05763dc090b7f5d21468633a7ead519a89a50d8db7bd4c8f + checksum: 359848cad18ae9a70d9767e07f9f520459b0794f32b9558ec442db92ea83f392a5f0b0fcab56804328e18bf4d1371a18ef2eaa5847c5e94603ff9a2dc1123a26 languageName: node linkType: hard From 5c4267337fcbe12f6e301e0161a14caa1bf82ef9 Mon Sep 17 00:00:00 2001 From: Djam Date: Mon, 5 Jun 2023 17:52:21 +0200 Subject: [PATCH 35/78] Update 2023-05-06-backstage-deploy-alpha.mdx Signed-off-by: Djam --- microsite/blog/2023-05-06-backstage-deploy-alpha.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx index 5c4c3d2164..cc0f3ee1a9 100644 --- a/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx +++ b/microsite/blog/2023-05-06-backstage-deploy-alpha.mdx @@ -73,6 +73,6 @@ So, please try this new CLI out and let us know what you think on [Discord](http Useful links: -- [Deploy docs](https://backstage.io/docs/deployment/pulumi/aws-lightsail) +- [Backstage Deploy](https://github.com/backstage/backstage-deploy) - [Pulumi docs](https://www.pulumi.com/docs/) - [#deployment](https://discord.com/channels/687207715902193673/995973463208644678) channel in Discord From 7dd8fa7aab2e240440e0e03d58396c681a3cf880 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 16:08:10 +0000 Subject: [PATCH 36/78] fix(deps): update dependency @google-cloud/storage to v6.11.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 50c2bb2d5b..0d8f98d807 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11280,8 +11280,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^6.0.0": - version: 6.10.1 - resolution: "@google-cloud/storage@npm:6.10.1" + version: 6.11.0 + resolution: "@google-cloud/storage@npm:6.11.0" dependencies: "@google-cloud/paginator": ^3.0.7 "@google-cloud/projectify": ^3.0.0 @@ -11300,7 +11300,7 @@ __metadata: retry-request: ^5.0.0 teeny-request: ^8.0.0 uuid: ^8.0.0 - checksum: 8908500e3057456ccac5cca17224807898a5c51c0423dec336e802ed0bc0785be7658b0050f848102a2f9207dbf3fb2649f6cc8e515c49fce3a134a8f158ecea + checksum: 4183c0ad3b271e0c22b3aa01e9942590ae64b984efa142a787b96379baaf4c05a2ebdbb6015c8ea189951df251d57f4f35c6a28b920922828cc89d3e4147d1a1 languageName: node linkType: hard From 024e9554f2f1b1b7f12193987b9ddaf0d9084fd6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 16:08:45 +0000 Subject: [PATCH 37/78] fix(deps): update dependency eslint-plugin-deprecation to v1.4.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 129 ++++++++++++++---------------------------------------- 1 file changed, 34 insertions(+), 95 deletions(-) diff --git a/yarn.lock b/yarn.lock index 50c2bb2d5b..ce636d7b04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17960,22 +17960,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/experimental-utils@npm:^5.0.0": - version: 5.9.0 - resolution: "@typescript-eslint/experimental-utils@npm:5.9.0" - dependencies: - "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.9.0 - "@typescript-eslint/types": 5.9.0 - "@typescript-eslint/typescript-estree": 5.9.0 - eslint-scope: ^5.1.1 - eslint-utils: ^3.0.0 - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 731b27840642b644e65f4ae321ed47e973ffadacd1aa24a19b02b4b298b5bcfbfa16c2d3d034e87a08c3c45f942c5b974f7619cb143eb23fb950f37418dce791 - languageName: node - linkType: hard - "@typescript-eslint/parser@npm:^5.9.0": version: 5.53.0 resolution: "@typescript-eslint/parser@npm:5.53.0" @@ -18003,23 +17987,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/scope-manager@npm:5.58.0" +"@typescript-eslint/scope-manager@npm:5.59.8": + version: 5.59.8 + resolution: "@typescript-eslint/scope-manager@npm:5.59.8" dependencies: - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/visitor-keys": 5.58.0 - checksum: f0d3df5cc3c461fe63ef89ad886b53c239cc7c1d9061d83d8a9d9c8e087e5501eac84bebff8a954728c17ccea191f235686373d54d2b8b6370af2bcf2b18e062 - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:5.9.0": - version: 5.9.0 - resolution: "@typescript-eslint/scope-manager@npm:5.9.0" - dependencies: - "@typescript-eslint/types": 5.9.0 - "@typescript-eslint/visitor-keys": 5.9.0 - checksum: 46e7ab0cef558e7faf1aa8d122a265e196566c0073292f5b2f9cede1f63f52860be8e4ef90251c15e0922339c15852584cb5337382035baff87f1203c0c8d1b5 + "@typescript-eslint/types": 5.59.8 + "@typescript-eslint/visitor-keys": 5.59.8 + checksum: e1e810ee991cfeb433330b04ee949bb6784abe4dbdb7d9480aa7a7536671b4fec914b7803edf662516c8ecb1b31dcff126797f9923270a529c26e2b00b0ea96f languageName: node linkType: hard @@ -18047,17 +18021,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/types@npm:5.58.0" - checksum: 8622a73d73220c4a7111537825f488c0271272032a1d4e129dc722bc6e8b3ec84f64469b2ca3b8dae7da3a9c18953ce1449af51f5f757dad60835eb579ad1d2c - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:5.9.0": - version: 5.9.0 - resolution: "@typescript-eslint/types@npm:5.9.0" - checksum: 7c4e142600aec266b41418dab1d0cee8cace980b6990692df6522de6eab6705bf515aef36180e4a38c62acb10c92fb474269ac6856a4266d6b035068cd83fad3 +"@typescript-eslint/types@npm:5.59.8": + version: 5.59.8 + resolution: "@typescript-eslint/types@npm:5.59.8" + checksum: 559473d5601c849eb0da1874a2ac67c753480beed484ad6f6cda62fa6023273f2c3005c7f2864d9c2afb7c6356412d0d304b57db10c53597207f18a7f6cd4f18 languageName: node linkType: hard @@ -18079,12 +18046,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.58.0" +"@typescript-eslint/typescript-estree@npm:5.59.8": + version: 5.59.8 + resolution: "@typescript-eslint/typescript-estree@npm:5.59.8" dependencies: - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/visitor-keys": 5.58.0 + "@typescript-eslint/types": 5.59.8 + "@typescript-eslint/visitor-keys": 5.59.8 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -18093,25 +18060,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 51b668ec858db0c040a71dff526273945cee4ba5a9b240528d503d02526685882d900cf071c6636a4d9061ed3fd4a7274f7f1a23fba55c4b48b143344b4009c7 - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:5.9.0": - version: 5.9.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.9.0" - dependencies: - "@typescript-eslint/types": 5.9.0 - "@typescript-eslint/visitor-keys": 5.9.0 - debug: ^4.3.2 - globby: ^11.0.4 - is-glob: ^4.0.3 - semver: ^7.3.5 - tsutils: ^3.21.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 71e3f720e335fb08e66950d32b723484aa4d1f4a3163e82259f4be2d11091545070c2e71472be470403cb6f82bf1abe84fa89c1d0b1d47adc8550b3f70aabfb5 + checksum: d93371cc866f573a6a1ddc0eb10d498a8e59f36763a99ce21da0737fff2b4c942eef1587216aad273f8d896ebc0b19003677cba63a27d2646aa2c087638963eb languageName: node linkType: hard @@ -18133,21 +18082,21 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/utils@npm:5.58.0" +"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.57.0, @typescript-eslint/utils@npm:^5.58.0": + version: 5.59.8 + resolution: "@typescript-eslint/utils@npm:5.59.8" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@types/json-schema": ^7.0.9 "@types/semver": ^7.3.12 - "@typescript-eslint/scope-manager": 5.58.0 - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/typescript-estree": 5.58.0 + "@typescript-eslint/scope-manager": 5.59.8 + "@typescript-eslint/types": 5.59.8 + "@typescript-eslint/typescript-estree": 5.59.8 eslint-scope: ^5.1.1 semver: ^7.3.7 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: c618ae67963ecf96b1492c09afaeb363f542f0d6780bcac4af3c26034e3b20034666b2d523aa94821df813aafb57a0b150a7d5c2224fe8257452ad1de2237a58 + checksum: cbaa057485c7f52c45d0dfb4f5a8e9273abccb1c52dcb4426a79f9e71d2c1062cf2525bad6d4aca5ec42db3fe723d749843bcade5a323bde7fbe4b5d5b5d5c3b languageName: node linkType: hard @@ -18161,23 +18110,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.58.0" +"@typescript-eslint/visitor-keys@npm:5.59.8": + version: 5.59.8 + resolution: "@typescript-eslint/visitor-keys@npm:5.59.8" dependencies: - "@typescript-eslint/types": 5.58.0 + "@typescript-eslint/types": 5.59.8 eslint-visitor-keys: ^3.3.0 - checksum: ab2d1f37660559954c840429ef78bbf71834063557e3e68e435005b4987970b9356fdf217ead53f7a57f66f5488dc478062c5c44bf17053a8bf041733539b98f - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:5.9.0": - version: 5.9.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.9.0" - dependencies: - "@typescript-eslint/types": 5.9.0 - eslint-visitor-keys: ^3.0.0 - checksum: 34a595b83b0e7d4f387d6c81b272804b94a1a91478c5f856fdfdd227595bf8562bf3f5d732606d10b4522c3f2617d09d4bacd2193f757a324ea66b3144a68903 + checksum: 6bfa7918dbb0e08d8a7404aeeef7bcd1a85736dc8d01614d267c0c5ec10f94d2746b50a945bf5c82c54fda67926e8deaeba8565c919da17f725fc11209ef8987 languageName: node linkType: hard @@ -24103,16 +24042,16 @@ __metadata: linkType: hard "eslint-plugin-deprecation@npm:^1.3.2": - version: 1.3.3 - resolution: "eslint-plugin-deprecation@npm:1.3.3" + version: 1.4.1 + resolution: "eslint-plugin-deprecation@npm:1.4.1" dependencies: - "@typescript-eslint/experimental-utils": ^5.0.0 + "@typescript-eslint/utils": ^5.57.0 tslib: ^2.3.1 tsutils: ^3.21.0 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: ^3.7.5 || ^4.0.0 - checksum: 5e14d7bc8245a14784623632d43a6210880a4aad4c029fd44686a5516c248287f32406ff230f6e9d238784854b24cd09da953ec8f73d9d19a7c4b3905884e432 + typescript: ^3.7.5 || ^4.0.0 || ^5.0.0 + checksum: 75c7535d820d1749664705724cc979d706da126a2277f2937467f70156a2c220b10c66670f18226801c9555e4cd02312d353936f14d5752c2d2c648455fe5769 languageName: node linkType: hard @@ -24280,7 +24219,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.0.0, eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1": +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1": version: 3.4.1 resolution: "eslint-visitor-keys@npm:3.4.1" checksum: f05121d868202736b97de7d750847a328fcfa8593b031c95ea89425333db59676ac087fa905eba438d0a3c5769632f828187e0c1a0d271832a2153c1d3661c2c From 856495adb036b1c009ee862f9d3e2eb6761df7f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:33:17 +0000 Subject: [PATCH 38/78] fix(deps): update dependency @keyv/redis to v2.6.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 943bd87958..7f430b63bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12680,11 +12680,11 @@ __metadata: linkType: hard "@keyv/redis@npm:^2.5.3": - version: 2.6.0 - resolution: "@keyv/redis@npm:2.6.0" + version: 2.6.1 + resolution: "@keyv/redis@npm:2.6.1" dependencies: ioredis: ^5.3.2 - checksum: f3e5dc086d0406cd2d12b8660eceef76124b56bbefe204bf152ceb4de536d6370fd60667cd4982936590e1084055a8368c406302c39be492fc45d632b3c06dba + checksum: c0c0aa7d0b449ef295ab65bcc38d42a2f6f606f49bf1d9bc95cd0276d60854c4d579d6dfce9e070f350a07987dfde9d452884ee05e8f87215c49c304b27a68ab languageName: node linkType: hard From 7d478b9bea086f9adf277eea1b8f2081caaedd9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:33:50 +0000 Subject: [PATCH 39/78] fix(deps): update dependency google-auth-library to v8.8.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 943bd87958..e0410836ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25833,13 +25833,13 @@ __metadata: languageName: node linkType: hard -"gcp-metadata@npm:^5.0.0": - version: 5.0.0 - resolution: "gcp-metadata@npm:5.0.0" +"gcp-metadata@npm:^5.2.0": + version: 5.2.0 + resolution: "gcp-metadata@npm:5.2.0" dependencies: gaxios: ^5.0.0 json-bigint: ^1.0.0 - checksum: b75635e564a39ef0a6ebbff0daa97cea37382c3d6f0d7a6a0406e03a275de804a74e3f3d410d5313a6c9dd3496023e96bc81474917fa8369974398eb069d9645 + checksum: 4e7ed589c814bb79cbf052b0eda1d5e219fbee030f4772eca27ec1e6e1faa85ba0ef3b17ea5c3fd51a54fc5429c924b4edbb260ac147701f211fb9807b893544 languageName: node linkType: hard @@ -26192,19 +26192,19 @@ __metadata: linkType: hard "google-auth-library@npm:^8.0.0, google-auth-library@npm:^8.0.1, google-auth-library@npm:^8.0.2": - version: 8.7.0 - resolution: "google-auth-library@npm:8.7.0" + version: 8.8.0 + resolution: "google-auth-library@npm:8.8.0" dependencies: arrify: ^2.0.0 base64-js: ^1.3.0 ecdsa-sig-formatter: ^1.0.11 fast-text-encoding: ^1.0.0 gaxios: ^5.0.0 - gcp-metadata: ^5.0.0 + gcp-metadata: ^5.2.0 gtoken: ^6.1.0 jws: ^4.0.0 lru-cache: ^6.0.0 - checksum: 978d1c5f763aceddbc0218cd76fa578c8ba54a0653cefffaf61847bb8d246ebf26e7fcd276d8885b8a3354c17eef0a11cfae9e60e4df62c01cae4378d4eb78e4 + checksum: 4552805466679e258febc4c0621d401d510df267f2f105957b8925b79c1454d9a2e5d53af211dd90a1848658f608babac7f5bc2f3c536c441ba32ba3641d335d languageName: node linkType: hard From 73cc0deee48ae0f1b58ade5c0acc4a6e786dd5e4 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Mon, 5 Jun 2023 16:45:48 -0400 Subject: [PATCH 40/78] feat: Proposed fix dialog (#17576) * feat: add errors to Pod Drawer Signed-off-by: Matthew Clarke * fix: lint fix Signed-off-by: Matthew Clarke * test: useMatchingErrorTest Signed-off-by: Matthew Clarke * feat: pod fix dialog for errors Signed-off-by: Matthew Clarke * fix: link Signed-off-by: Matthew Clarke * fix: icon tweak Signed-off-by: Matthew Clarke --------- Signed-off-by: Matthew Clarke --- .changeset/wise-bats-repair.md | 6 + .../dice-roller/dice-roller-manifests.yaml | 36 ++++ plugins/kubernetes/api-report.md | 195 +++++++++++++++++- .../src/api/KubernetesProxyClient.test.ts | 70 +++++++ .../src/api/KubernetesProxyClient.ts | 44 ++++ plugins/kubernetes/src/api/types.ts | 7 + .../Pods/ErrorList/ErrorList.test.tsx | 16 +- .../components/Pods/ErrorList/ErrorList.tsx | 33 ++- .../components/Pods/Events/Events.test.tsx | 96 +++++++++ .../src/components/Pods/Events/Events.tsx | 154 ++++++++++++++ .../src/components/Pods/Events/index.ts | 17 ++ .../components/Pods/Events/useEvents.test.tsx | 68 ++++++ .../src/components/Pods/Events/useEvents.ts | 49 +++++ .../Pods/FixDialog/FixDialog.test.tsx | 159 ++++++++++++++ .../components/Pods/FixDialog/FixDialog.tsx | 187 +++++++++++++++++ .../src/components/Pods/FixDialog/index.ts | 16 ++ .../Pods/PodDrawer/ContainerCard.tsx | 12 +- .../Pods/PodDrawer/PendingPodContent.tsx | 14 +- .../Pods/PodDrawer/PodDrawer.test.tsx | 2 +- .../components/Pods/PodDrawer/PodDrawer.tsx | 11 + .../src/components/Pods/PodDrawer/index.ts | 4 +- .../src/components/Pods/PodLogs/PodLogs.tsx | 43 +++- .../components/Pods/PodLogs/PodLogsDialog.tsx | 22 +- .../src/components/Pods/PodLogs/types.ts | 10 + .../src/components/Pods/PodLogs/usePodLogs.ts | 28 ++- .../kubernetes/src/components/Pods/index.ts | 7 +- .../kubernetes/src/components/Pods/types.ts | 5 + .../src/error-detection/deployments.ts | 1 - .../error-detection/error-detection.test.ts | 63 +++++- .../kubernetes/src/error-detection/hpas.ts | 1 - .../kubernetes/src/error-detection/pods.ts | 131 +++++++++++- .../kubernetes/src/error-detection/types.ts | 7 +- .../src/hooks/useMatchingErrors.test.tsx | 16 +- 33 files changed, 1457 insertions(+), 73 deletions(-) create mode 100644 .changeset/wise-bats-repair.md create mode 100644 plugins/kubernetes/src/components/Pods/Events/Events.test.tsx create mode 100644 plugins/kubernetes/src/components/Pods/Events/Events.tsx create mode 100644 plugins/kubernetes/src/components/Pods/Events/index.ts create mode 100644 plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx create mode 100644 plugins/kubernetes/src/components/Pods/Events/useEvents.ts create mode 100644 plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx create mode 100644 plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx create mode 100644 plugins/kubernetes/src/components/Pods/FixDialog/index.ts diff --git a/.changeset/wise-bats-repair.md b/.changeset/wise-bats-repair.md new file mode 100644 index 0000000000..fc383eb11d --- /dev/null +++ b/.changeset/wise-bats-repair.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes': patch +--- + +Add proposed fix dialog for pod errors diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml index a1a2e105b3..8f225e666a 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml +++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml @@ -32,6 +32,42 @@ spec: ports: - containerPort: 80 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dice-roller-incorrect-probe + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + matchLabels: + app: dice-roller-incorrect-probe + replicas: 2 + template: + metadata: + labels: + app: dice-roller-incorrect-probe + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: nginx + image: nginx:1.14.2 + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '50m' + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + ports: + - containerPort: 80 + --- apiVersion: apps/v1 kind: Deployment diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 823b9c89a2..e117e3627d 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -6,6 +6,7 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; +import { AsyncState } from 'react-use/lib/useAsyncFn'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ClientPodStatus } from '@backstage/plugin-kubernetes-common'; import { ClusterAttributes } from '@backstage/plugin-kubernetes-common'; @@ -14,6 +15,9 @@ import { CustomObjectsByEntityRequest } from '@backstage/plugin-kubernetes-commo import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { Event as Event_2 } from 'kubernetes-models/v1'; +import { IContainer } from 'kubernetes-models/v1'; +import { IContainerStatus } from 'kubernetes-models/v1'; import { IdentityApi } from '@backstage/core-plugin-api'; import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta'; import type { JsonObject } from '@backstage/types'; @@ -21,8 +25,8 @@ import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { OAuthApi } from '@backstage/core-plugin-api'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; -import { Pod } from 'kubernetes-models/v1'; -import { Pod as Pod_2 } from 'kubernetes-models/v1/Pod'; +import { Pod } from 'kubernetes-models/v1/Pod'; +import { Pod as Pod_2 } from 'kubernetes-models/v1'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { V1ConfigMap } from '@kubernetes/client-node'; @@ -78,6 +82,25 @@ export interface ClusterLinksFormatterOptions { // @public (undocumented) export const clusterLinksFormatters: Record; +// @public +export const ContainerCard: React_2.FC; + +// @public +export interface ContainerCardProps { + // (undocumented) + containerSpec?: IContainer; + // (undocumented) + containerStatus: IContainerStatus; + // (undocumented) + podScope: PodScope; +} + +// @public +export interface ContainerScope extends PodScope { + // (undocumented) + containerName: string; +} + // Warning: (ae-forgotten-export) The symbol "CronJobsAccordionsProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CronJobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -113,7 +136,7 @@ export interface DetectedError { // Warning: (ae-forgotten-export) The symbol "ProposedFix" needs to be exported by the entry point index.d.ts // // (undocumented) - proposedFix: ProposedFix[]; + proposedFix?: ProposedFix; // (undocumented) severity: ErrorSeverity; // Warning: (ae-forgotten-export) The symbol "ResourceRef" needs to be exported by the entry point index.d.ts @@ -144,6 +167,15 @@ export type EntityKubernetesContentProps = { refreshIntervalMs?: number; }; +// @public +export const ErrorList: ({ podAndErrors }: ErrorListProps) => JSX.Element; + +// @public +export interface ErrorListProps { + // (undocumented) + podAndErrors: PodAndErrors[]; +} + // Warning: (ae-forgotten-export) The symbol "ErrorPanelProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -165,6 +197,65 @@ export const ErrorReporting: ({ // @public export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; +// @public +export const Events: ({ + involvedObjectName, + namespace, + clusterName, + warningEventsOnly, +}: EventsProps) => JSX.Element; + +// @public +export const EventsContent: ({ + events, + warningEventsOnly, +}: EventsContentProps) => JSX.Element; + +// @public +export interface EventsContentProps { + // (undocumented) + events: Event_2[]; + // (undocumented) + warningEventsOnly?: boolean; +} + +// @public +export interface EventsOptions { + // (undocumented) + clusterName: string; + // (undocumented) + involvedObjectName: string; + // (undocumented) + namespace: string; +} + +// @public +export interface EventsProps { + // (undocumented) + clusterName: string; + // (undocumented) + involvedObjectName: string; + // (undocumented) + namespace: string; + // (undocumented) + warningEventsOnly?: boolean; +} + +// @public +export const FixDialog: React_2.FC; + +// @public +export interface FixDialogProps { + // (undocumented) + clusterName: string; + // (undocumented) + error: DetectedError; + // (undocumented) + open?: boolean; + // (undocumented) + pod: Pod; +} + // Warning: (ae-forgotten-export) The symbol "FormatClusterLinkOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "formatClusterLink" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -416,12 +507,19 @@ export { kubernetesPlugin as plugin }; // // @public (undocumented) export interface KubernetesProxyApi { + // (undocumented) + getEventsByInvolvedObjectName(request: { + clusterName: string; + involvedObjectName: string; + namespace: string; + }): Promise; // (undocumented) getPodLogs(request: { podName: string; namespace: string; clusterName: string; containerName: string; + previous?: boolean; }): Promise<{ text: string; }>; @@ -436,16 +534,28 @@ export const kubernetesProxyApiRef: ApiRef; export class KubernetesProxyClient { constructor(options: { kubernetesApi: KubernetesApi }); // (undocumented) + getEventsByInvolvedObjectName({ + clusterName, + involvedObjectName, + namespace, + }: { + clusterName: string; + involvedObjectName: string; + namespace: string; + }): Promise; + // (undocumented) getPodLogs({ podName, namespace, clusterName, containerName, + previous, }: { podName: string; namespace: string; clusterName: string; containerName: string; + previous?: boolean; }): Promise<{ text: string; }>; @@ -476,12 +586,62 @@ export const LinkErrorPanel: ({ errorMessage, }: ErrorPanelProps_2) => JSX.Element; +// @public +export const PendingPodContent: ({ + pod, +}: PendingPodContentProps) => JSX.Element; + +// @public +export interface PendingPodContentProps { + // (undocumented) + pod: Pod_2; +} + +// @public +export interface PodAndErrors { + // (undocumented) + clusterName: string; + // (undocumented) + errors: DetectedError[]; + // (undocumented) + pod: Pod_2; +} + // Warning: (ae-forgotten-export) The symbol "PodDrawerProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PodDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export const PodDrawer: ({ podAndErrors, open }: PodDrawerProps) => JSX.Element; +// @public +export const PodLogs: React_2.FC; + +// @public +export const PodLogsDialog: ({ + containerScope, +}: PodLogsDialogProps) => JSX.Element; + +// @public +export interface PodLogsDialogProps { + // (undocumented) + containerScope: ContainerScope; +} + +// @public +export interface PodLogsOptions { + // (undocumented) + containerScope: ContainerScope; + // (undocumented) + previous?: boolean; +} + +// @public +export interface PodLogsProps { + // (undocumented) + containerScope: ContainerScope; + // (undocumented) + previous?: boolean; +} + // Warning: (ae-missing-release-tag) "PodNamesWithErrorsContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -494,6 +654,16 @@ export const PodNamesWithMetricsContext: React_2.Context< Map >; +// @public +export interface PodScope { + // (undocumented) + clusterName: string; + // (undocumented) + podName: string; + // (undocumented) + podNamespace: string; +} + // Warning: (ae-forgotten-export) The symbol "PodsTablesProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PodsTable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -533,6 +703,13 @@ export const useCustomResources: ( intervalMs?: number, ) => KubernetesObjects; +// @public +export const useEvents: ({ + involvedObjectName, + namespace, + clusterName, +}: EventsOptions) => AsyncState; + // Warning: (ae-missing-release-tag) "useKubernetesObjects" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -540,4 +717,12 @@ export const useKubernetesObjects: ( entity: Entity, intervalMs?: number, ) => KubernetesObjects; + +// @public +export const usePodLogs: ({ + containerScope, + previous, +}: PodLogsOptions) => AsyncState<{ + text: string; +}>; ``` diff --git a/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts b/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts index 6db1e459be..6b0be6de6c 100644 --- a/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts +++ b/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { DateTime } from 'luxon'; import { KubernetesProxyClient } from './KubernetesProxyClient'; describe('KubernetesProxyClient', () => { let proxy: KubernetesProxyClient; const callProxyMock = jest.fn(); + const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); beforeEach(() => { jest.resetAllMocks(); @@ -51,4 +53,72 @@ describe('KubernetesProxyClient', () => { path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container', }); }); + it('/logs returns log text - crash logs', async () => { + const request = { + podName: 'some-pod', + namespace: 'some-namespace', + clusterName: 'some-cluster', + containerName: 'some-container', + previous: true, + }; + + callProxyMock.mockResolvedValue({ + text: jest.fn().mockResolvedValue('Hello World'), + ok: true, + }); + + const response = await proxy.getPodLogs(request); + await expect(response).toStrictEqual({ text: 'Hello World' }); + expect(callProxyMock).toHaveBeenCalledWith({ + clusterName: 'some-cluster', + init: { + method: 'GET', + }, + path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container&previous=', + }); + }); + it('/getEventsByInvolvedObjectName returns events', async () => { + const request = { + clusterName: 'some-cluster', + involvedObjectName: 'some-object', + namespace: 'some-namespace', + }; + const events = [ + { + type: 'Warning', + message: 'uh oh', + reason: 'something happened', + count: 23, + metadata: { + creationTimestamp: oneHourAgo, + }, + }, + { + type: 'Info', + message: 'hello there', + reason: 'something happened', + count: 52, + metadata: { + creationTimestamp: oneHourAgo, + }, + }, + ]; + + callProxyMock.mockResolvedValue({ + json: jest.fn().mockResolvedValue({ + items: events, + }), + ok: true, + }); + + const response = await proxy.getEventsByInvolvedObjectName(request); + await expect(response).toStrictEqual(events); + expect(callProxyMock).toHaveBeenCalledWith({ + clusterName: 'some-cluster', + init: { + method: 'GET', + }, + path: '/api/v1/namespaces/some-namespace/events?fieldSelector=involvedObject.name=some-object', + }); + }); }); diff --git a/plugins/kubernetes/src/api/KubernetesProxyClient.ts b/plugins/kubernetes/src/api/KubernetesProxyClient.ts index 8fd4674ddd..3703d8b180 100644 --- a/plugins/kubernetes/src/api/KubernetesProxyClient.ts +++ b/plugins/kubernetes/src/api/KubernetesProxyClient.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { KubernetesApi } from './types'; +import { Event } from 'kubernetes-models/v1'; /** * A client for common requests through the proxy endpoint of the kubernetes backend plugin. @@ -41,20 +42,63 @@ export class KubernetesProxyClient { return await response.text(); } + private async handleJson(response: Response): Promise { + if (!response.ok) { + const payload = await response.text(); + let message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; + switch (response.status) { + case 404: + message = `Proxy request failed with ${response.status} ${response.statusText}, ${payload}`; + break; + default: + message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; + } + throw new Error(message); + } + + return await response.json(); + } + + async getEventsByInvolvedObjectName({ + clusterName, + involvedObjectName, + namespace, + }: { + clusterName: string; + involvedObjectName: string; + namespace: string; + }): Promise { + return await this.kubernetesApi + .proxy({ + clusterName, + path: `/api/v1/namespaces/${namespace}/events?fieldSelector=involvedObject.name=${involvedObjectName}`, + init: { + method: 'GET', + }, + }) + .then(response => this.handleJson(response)) + .then(eventList => eventList.items); + } + async getPodLogs({ podName, namespace, clusterName, containerName, + previous, }: { podName: string; namespace: string; clusterName: string; containerName: string; + previous?: boolean; }): Promise<{ text: string }> { const params = new URLSearchParams({ container: containerName, }); + if (previous) { + params.append('previous', ''); + } return await this.kubernetesApi .proxy({ clusterName: clusterName, diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 5dec189982..6065e47169 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -21,6 +21,7 @@ import { CustomObjectsByEntityRequest, } from '@backstage/plugin-kubernetes-common'; import { createApiRef } from '@backstage/core-plugin-api'; +import { Event } from 'kubernetes-models/v1'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', @@ -60,5 +61,11 @@ export interface KubernetesProxyApi { namespace: string; clusterName: string; containerName: string; + previous?: boolean; }): Promise<{ text: string }>; + getEventsByInvolvedObjectName(request: { + clusterName: string; + involvedObjectName: string; + namespace: string; + }): Promise; } diff --git a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx index f28580b509..3ee755becb 100644 --- a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx +++ b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx @@ -45,15 +45,13 @@ describe('ErrorList', () => { kind: 'Pod', apiGroup: 'v1', }, - proposedFix: [ - { - type: 'logs', - container: 'some-container', - errorType: 'some error type', - rootCauseExplanation: 'some root cause', - possibleFixes: ['fix1', 'fix2'], - }, - ], + proposedFix: { + type: 'logs', + container: 'some-container', + errorType: 'some error type', + rootCauseExplanation: 'some root cause', + actions: ['fix1', 'fix2'], + }, }, ], }, diff --git a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx index b7b3418889..4d1d7a3033 100644 --- a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx +++ b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx @@ -24,8 +24,10 @@ import { makeStyles, Theme, Paper, + Grid, } from '@material-ui/core'; import { PodAndErrors } from '../types'; +import { FixDialog } from '../FixDialog/FixDialog'; const useStyles = makeStyles((_theme: Theme) => createStyles({ @@ -38,10 +40,20 @@ const useStyles = makeStyles((_theme: Theme) => }), ); -interface ErrorListProps { +/** + * Props for ErrorList + * + * @public + */ +export interface ErrorListProps { podAndErrors: PodAndErrors[]; } +/** + * Shows a list of errors found on a Pod + * + * @public + */ export const ErrorList = ({ podAndErrors }: ErrorListProps) => { const classes = useStyles(); return ( @@ -59,10 +71,21 @@ export const ErrorList = ({ podAndErrors }: ErrorListProps) => { > {i > 0 && } - + + + + + + + + ); diff --git a/plugins/kubernetes/src/components/Pods/Events/Events.test.tsx b/plugins/kubernetes/src/components/Pods/Events/Events.test.tsx new file mode 100644 index 0000000000..8323666e66 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/Events/Events.test.tsx @@ -0,0 +1,96 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EventsContent } from './Events'; +import { render } from '@testing-library/react'; +import { Event } from 'kubernetes-models/v1'; +import { DateTime } from 'luxon'; + +describe('EventsContent', () => { + const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + it('should show info events', () => { + const { getByText } = render( + , + ); + expect(getByText('First event 1 hour ago (count: 52)')).toBeInTheDocument(); + expect(getByText('something happened: hello there')).toBeInTheDocument(); + }); + it('should show warning events', () => { + const { getByText } = render( + , + ); + expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument(); + expect(getByText('something happened: uh oh')).toBeInTheDocument(); + }); + + it('should only show warning events when warningEventsOnly set', () => { + const { getByText, queryByText } = render( + , + ); + expect(queryByText('First event 1 hour ago (count: 52)')).toBeNull(); + expect(queryByText('something happened: hello there')).toBeNull(); + expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument(); + expect(getByText('something happened: uh oh')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/Events/Events.tsx b/plugins/kubernetes/src/components/Pods/Events/Events.tsx new file mode 100644 index 0000000000..9f888d182c --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/Events/Events.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { + Avatar, + Container, + Grid, + List, + ListItem, + ListItemAvatar, + ListItemText, + Tooltip, + Typography, +} from '@material-ui/core'; + +import InfoIcon from '@material-ui/icons/Info'; +import WarningIcon from '@material-ui/icons/Warning'; +import { DateTime } from 'luxon'; + +import { useEvents } from './useEvents'; +import { Skeleton } from '@material-ui/lab'; +import { DismissableBanner } from '@backstage/core-components'; +import { Event } from 'kubernetes-models/v1'; + +/** + * Props for Events + * + * @public + */ +export interface EventsContentProps { + warningEventsOnly?: boolean; + events: Event[]; +} + +const getAvatarByType = (type?: string) => { + return ( + + {type === 'Warning' ? : } + + ); +}; + +/** + * Shows given Kubernetes events + * + * @public + */ +export const EventsContent = ({ + events, + warningEventsOnly, +}: EventsContentProps) => { + if (events.length === 0) { + return No events found; + } + + return ( + + + + {events + .filter(event => { + if (warningEventsOnly) { + return event.type === 'Warning'; + } + return true; + }) + .map(event => { + const timeAgo = event.metadata.creationTimestamp + ? DateTime.fromISO(event.metadata.creationTimestamp).toRelative( + { + locale: 'en', + }, + ) + : 'unknown'; + return ( + + + {getAvatarByType(event.type)} + + + + ); + })} + + + + ); +}; + +/** + * Props for Events + * + * @public + */ +export interface EventsProps { + involvedObjectName: string; + namespace: string; + clusterName: string; + warningEventsOnly?: boolean; +} + +/** + * Retrieves and shows Kubernetes events for the given object + * + * @public + */ +export const Events = ({ + involvedObjectName, + namespace, + clusterName, + warningEventsOnly, +}: EventsProps) => { + const { value, error, loading } = useEvents({ + involvedObjectName, + namespace, + clusterName, + }); + + return ( + <> + {error && ( + + )} + {loading && } + {!loading && value !== undefined && ( + + )} + + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/Events/index.ts b/plugins/kubernetes/src/components/Pods/Events/index.ts new file mode 100644 index 0000000000..da6c4102fa --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/Events/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './Events'; +export * from './useEvents'; diff --git a/plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx b/plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx new file mode 100644 index 0000000000..15da221f64 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useApi } from '@backstage/core-plugin-api'; +import { renderHook } from '@testing-library/react-hooks'; +import { useEvents } from './useEvents'; +import { DateTime } from 'luxon'; + +jest.mock('@backstage/core-plugin-api'); + +jest.mock('@backstage/plugin-kubernetes', () => ({ + kubernetesProxyApiRef: () => jest.fn(), +})); + +const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + +const response = [ + { + type: 'Info', + message: 'hello there', + reason: 'something happened', + count: 52, + metadata: { + creationTimestamp: oneHourAgo, + }, + }, +] as any; + +describe('Events', () => { + const mockGetEventsByInvolvedObjectName = jest.fn(); + afterEach(() => { + jest.resetAllMocks(); + }); + it('should fetch and show events', async () => { + (useApi as any).mockReturnValue({ + getEventsByInvolvedObjectName: + mockGetEventsByInvolvedObjectName.mockResolvedValue(response), + }); + + const { result, waitForNextUpdate } = renderHook(() => + useEvents({ + involvedObjectName: 'some-objecgt', + namespace: 'some-namespace', + clusterName: 'some-cluster', + }), + ); + + expect(result.current.loading).toEqual(true); + + await waitForNextUpdate(); + + expect(result.current.error).toBeUndefined(); + expect(result.current.loading).toEqual(false); + expect(result.current.value).toStrictEqual(response); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/Events/useEvents.ts b/plugins/kubernetes/src/components/Pods/Events/useEvents.ts new file mode 100644 index 0000000000..441937507d --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/Events/useEvents.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { kubernetesProxyApiRef } from '../../../api'; + +/** + * Arguments for useEvents + * + * @public + */ +export interface EventsOptions { + involvedObjectName: string; + namespace: string; + clusterName: string; +} + +/** + * Retrieves the events for the given object + * + * @public + */ +export const useEvents = ({ + involvedObjectName, + namespace, + clusterName, +}: EventsOptions) => { + const kubernetesProxyApi = useApi(kubernetesProxyApiRef); + return useAsync(async () => { + return await kubernetesProxyApi.getEventsByInvolvedObjectName({ + involvedObjectName, + namespace, + clusterName, + }); + }, [involvedObjectName, namespace, clusterName]); +}; diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx new file mode 100644 index 0000000000..e779ab5f0d --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { render } from '@testing-library/react'; + +import { FixDialog } from './FixDialog'; +import { Pod } from 'kubernetes-models/v1/Pod'; + +jest.mock('../Events', () => ({ + Events: () => { + return ; + }, +})); + +jest.mock('../PodLogs', () => ({ + PodLogs: () => { + return ; + }, +})); + +describe('FixDialog', () => { + it('docs link should render', () => { + const { getByText } = render( + , + ); + expect(getByText('Open docs')).toBeInTheDocument(); + expect(getByText('some error message')).toBeInTheDocument(); + expect(getByText('some-pod - some error type')).toBeInTheDocument(); + expect(getByText('some root cause')).toBeInTheDocument(); + expect(getByText('fix1')).toBeInTheDocument(); + expect(getByText('fix2')).toBeInTheDocument(); + }); + it('events button should render', () => { + const { getByText } = render( + , + ); + expect(getByText('Events:')).toBeInTheDocument(); + expect(getByText('some error message')).toBeInTheDocument(); + expect(getByText('some-pod - some error type')).toBeInTheDocument(); + expect(getByText('some root cause')).toBeInTheDocument(); + expect(getByText('fix1')).toBeInTheDocument(); + expect(getByText('fix2')).toBeInTheDocument(); + }); + it('Logs button should render', () => { + const { getByText } = render( + , + ); + expect(getByText('Crash logs:')).toBeInTheDocument(); + expect(getByText('some error message')).toBeInTheDocument(); + expect(getByText('some-pod - some error type')).toBeInTheDocument(); + expect(getByText('some root cause')).toBeInTheDocument(); + expect(getByText('fix1')).toBeInTheDocument(); + expect(getByText('fix2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx new file mode 100644 index 0000000000..63ce4af03b --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx @@ -0,0 +1,187 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; + +import { Button, Grid } from '@material-ui/core'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import IconButton from '@material-ui/core/IconButton'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import CloseIcon from '@material-ui/icons/Close'; +import HelpIcon from '@material-ui/icons/Help'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; + +import { Pod } from 'kubernetes-models/v1/Pod'; +import { DetectedError } from '../../../error-detection'; +import { PodLogs } from '../PodLogs'; +import { Events } from '../Events'; +import { LinkButton } from '@backstage/core-components'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }), +); + +/** + * Props for FixDialog + * + * @public + */ +export interface FixDialogProps { + open?: boolean; + clusterName: string; + pod: Pod; + error: DetectedError; +} + +/** + * A dialog for fixing detected Kubernetes errors + * + * @public + */ +export const FixDialog: React.FC = ({ + open, + pod, + error, + clusterName, +}: FixDialogProps) => { + const [isOpen, setOpen] = useState(!!open); + const classes = useStyles(); + + const openDialog = () => { + setOpen(true); + }; + + const closeDialog = () => { + setOpen(false); + }; + + const pf = error.proposedFix; + + const dialogContent = () => { + return ( + + + Detected error: + {error.message} + + + Cause explanation: + + {error.proposedFix?.rootCauseExplanation ?? 'unknown'} + + + + Fix: + +
    + {(error.proposedFix?.actions ?? []).map((fix, i) => { + return ( +
  • + {fix} +
  • + ); + })} +
+
+
+ + {pf && pf.type === 'logs' && ( + <> + + Crash logs: + + + + + + )} + {pf && pf.type === 'events' && ( + <> + + Events: + + + + + + )} +
+ ); + }; + + return ( + <> + + + + {pod.metadata?.name} - {error.type} + + + + + {dialogContent()} + + {pf && pf.type === 'docs' && ( + } + target="_blank" + rel="noopener" + > + Open docs + + )} + + + + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/index.ts b/plugins/kubernetes/src/components/Pods/FixDialog/index.ts new file mode 100644 index 0000000000..35601f9bd6 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/FixDialog/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './FixDialog'; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx index eac64ae741..970982d7a3 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -83,12 +83,22 @@ const ContainerDatetime = ({ prefix, dateTime }: ContainerDatetimeProps) => { ); }; +/** + * Props for ContainerCard + * + * @public + */ export interface ContainerCardProps { podScope: PodScope; containerSpec?: IContainer; containerStatus: IContainerStatus; } +/** + * Shows details about a container within a pod + * + * @public + */ export const ContainerCard: React.FC = ({ podScope, containerSpec, @@ -162,7 +172,7 @@ export const ContainerCard: React.FC = ({ { +const PodCondition = ({ condition }: PodConditionProps) => { return ( <> {condition.status === 'False' && ( @@ -64,10 +64,20 @@ export const PodCondition = ({ condition }: PodConditionProps) => { ); }; -interface PendingPodContentProps { +/** + * Props for PendingPodContent + * + * @public + */ +export interface PendingPodContentProps { pod: Pod; } +/** + * Shows details about pod's conditions as it starts + * + * @public + */ export const PendingPodContent = ({ pod }: PendingPodContentProps) => { // TODO add PodHasNetwork when it's out of alpha // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx index 64fd38c8d7..2b71976d8a 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx @@ -69,7 +69,7 @@ describe('PodDrawer', () => { container: 'some-container', errorType: 'some error type', rootCauseExplanation: 'some root cause', - possibleFixes: ['fix1', 'fix2'], + actions: ['fix1', 'fix2'], }, ], }, diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx index 5e49410dd4..8e616df14e 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx @@ -58,11 +58,22 @@ const useDrawerContentStyles = makeStyles((_theme: Theme) => function getContainerSpecByName(pod: Pod, containerName: string) { return pod.spec?.containers.find(c => c.name === containerName); } + +/** + * Props for PodDrawer + * + * @public + */ interface PodDrawerProps { open?: boolean; podAndErrors: PodAndErrors; } +/** + * A Drawer for Kubernetes Pods + * + * @public + */ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => { const classes = useDrawerContentStyles(); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts b/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts index 17d50ddfcd..92bb254c48 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { PodDrawer } from './PodDrawer'; +export * from './PodDrawer'; +export * from './ContainerCard'; +export * from './PendingPodContent'; diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx index 5cff023772..de9341508e 100644 --- a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx +++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx @@ -15,20 +15,39 @@ */ import React from 'react'; -import { DismissableBanner, LogViewer } from '@backstage/core-components'; +import { + DismissableBanner, + EmptyState, + LogViewer, +} from '@backstage/core-components'; import { Paper } from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; import { ContainerScope } from './types'; import { usePodLogs } from './usePodLogs'; -interface PodLogsProps { - podScope: ContainerScope; +/** + * Props for PodLogs + * + * @public + */ +export interface PodLogsProps { + containerScope: ContainerScope; + previous?: boolean; } -export const PodLogs: React.FC = ({ podScope }: PodLogsProps) => { +/** + * Shows the logs for the given pod + * + * @public + */ +export const PodLogs: React.FC = ({ + containerScope, + previous, +}: PodLogsProps) => { const { value, error, loading } = usePodLogs({ - podScope: podScope, + containerScope, + previous, }); return ( @@ -45,10 +64,20 @@ export const PodLogs: React.FC = ({ podScope }: PodLogsProps) => { )} {loading && } - {!loading && value !== undefined && } + {!loading && + value !== undefined && + (value.text === '' ? ( + + ) : ( + + ))} ); diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx index 05d980b26a..48fda2de2a 100644 --- a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx +++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx @@ -43,11 +43,21 @@ const useStyles = makeStyles((theme: Theme) => }), ); -interface PodLogsDialogProps { - podScope: ContainerScope; +/** + * Props for PodLogsDialog + * + * @public + */ +export interface PodLogsDialogProps { + containerScope: ContainerScope; } -export const PodLogsDialog = ({ podScope }: PodLogsDialogProps) => { +/** + * Shows the logs for the given pod in a Dialog + * + * @public + */ +export const PodLogsDialog = ({ containerScope }: PodLogsDialogProps) => { const classes = useStyles(); const [open, setOpen] = useState(false); @@ -62,8 +72,8 @@ export const PodLogsDialog = ({ podScope }: PodLogsDialogProps) => { <> - {podScope.podName} - {podScope.containerName} logs on cluster{' '} - {podScope.clusterName} + {containerScope.podName} - {containerScope.containerName} logs on + cluster {containerScope.clusterName} { - +