From da661f836e6a730f2a157feb8ec367d7603c9ab0 Mon Sep 17 00:00:00 2001 From: Debajyoti Halder Date: Tue, 26 Jan 2021 12:29:48 +0530 Subject: [PATCH 001/158] Update eslint rules --- packages/cli/config/eslint.backend.js | 4 ++-- packages/cli/config/eslint.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 7acb538d38..6739d1c2ce 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -38,8 +38,8 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], rules: { - '@typescript-eslint/no-shadow': 'off', - '@typescript-eslint/no-redeclare': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-redeclare': 'error', 'no-console': 0, // Permitted in console programs 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 044d737d15..c440e53bc0 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -45,9 +45,9 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], rules: { - '@typescript-eslint/no-shadow': 'off', - '@typescript-eslint/no-redeclare': 'off', - 'no-undef': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-redeclare': 'error', + 'import/newline-after-import': 'error', 'import/no-duplicates': 'warn', 'import/no-extraneous-dependencies': [ From 6106345e6d139a64a7713e066a670a9bdff002db Mon Sep 17 00:00:00 2001 From: Debajyoti Halder Date: Fri, 29 Jan 2021 11:55:12 +0530 Subject: [PATCH 002/158] Update project with latest eslint rules --- .../src/reading/tree/TarArchiveResponse.ts | 4 +-- .../src/reading/tree/ZipArchiveResponse.ts | 10 +++---- packages/cli/config/eslint.backend.js | 2 ++ packages/cli/config/eslint.js | 2 ++ packages/cli/src/commands/config/print.ts | 4 +-- .../cli/src/commands/versions/bump.test.ts | 4 +-- .../lib/bundler/LinkedPackageResolvePlugin.ts | 2 +- packages/cli/src/lib/bundler/paths.ts | 6 ++--- .../config-loader/src/lib/schema/collect.ts | 4 ++- .../lib/AuthConnector/DirectAuthConnector.ts | 10 +++---- packages/core-api/src/routing/FlatRoutes.tsx | 4 +-- .../core/src/layout/HeaderTabs/HeaderTabs.tsx | 4 +-- packages/e2e-test/src/commands/run.ts | 6 ++--- .../integration/src/ScmIntegrations.test.ts | 8 +++--- .../github/GithubCredentialsProvider.test.ts | 12 ++++----- .../src/github/GithubCredentialsProvider.ts | 2 +- .../src/stages/publish/local.test.ts | 4 +-- .../ApiDefinitionCard.test.tsx | 4 +-- .../src/catalog/DatabaseEntitiesCatalog.ts | 2 +- ...sOrganizationCloudAccountProcessor.test.ts | 9 ++++--- .../processors/BuiltinKindsEntityProcessor.ts | 4 +-- .../src/components/ImportComponentForm.tsx | 14 +++++----- .../src/components/ImportComponentPage.tsx | 12 ++++----- .../components/EntityLayout/EntityLayout.tsx | 6 +++-- .../components/EntityLayout/TabbedLayout.tsx | 4 +-- .../components/EntitySwitch/EntitySwitch.tsx | 4 +-- plugins/catalog/src/components/isOwnerOf.ts | 8 +++--- .../AlertActionCardList.tsx | 4 +-- .../src/components/BarChart/BarChartLabel.tsx | 4 +-- .../src/components/BarChart/BarChartSteps.tsx | 4 +-- .../ProductInsights/ProductInsights.tsx | 27 ++++++++++--------- plugins/cost-insights/src/utils/change.ts | 4 +-- plugins/jenkins/src/components/useBuilds.ts | 10 +++---- .../getKubernetesObjectsForServiceHandler.ts | 6 ++--- .../DeploymentDrawer.tsx | 10 +++---- .../HorizontalPodAutoscalerDrawer.tsx | 14 +++++----- .../IngressesAccordions/IngressDrawer.tsx | 4 +-- .../src/components/Pods/PodDrawer.tsx | 16 +++++------ .../ServicesAccordions/ServiceDrawer.tsx | 4 +-- plugins/kubernetes/src/utils/pod.tsx | 8 +++--- .../Group/GroupProfile/GroupProfileCard.tsx | 4 +-- plugins/org/src/components/isOwnerOf.ts | 8 +++--- .../Escalation/EscalationPolicy.tsx | 4 +-- .../TriggerDialog/TriggerDialog.tsx | 4 +-- .../stages/prepare/bitbucket.test.ts | 8 +++--- .../scaffolder/stages/prepare/github.test.ts | 8 +++--- .../MultistepJsonForm/MultistepJsonForm.tsx | 4 +-- .../components/TemplatePage/TemplatePage.tsx | 12 +++++---- .../components/SearchResult/SearchResult.tsx | 8 +++--- 49 files changed, 173 insertions(+), 157 deletions(-) diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index 5927eb75a1..05dd140b16 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -15,7 +15,7 @@ */ import tar, { Parse, ParseStream, ReadEntry } from 'tar'; -import path from 'path'; +import platformPath from 'path'; import fs from 'fs-extra'; import { Readable, pipeline as pipelineCb } from 'stream'; import { promisify } from 'util'; @@ -136,7 +136,7 @@ export class TarArchiveResponse implements ReadTreeResponse { const dir = options?.targetDir ?? - (await fs.mkdtemp(path.join(this.workDir, 'backstage-'))); + (await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-'))); const strip = this.subPath ? this.subPath.split('/').length - 1 : 0; diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 07d34faaa3..15bc558132 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import path from 'path'; +import platformPath from 'path'; import fs from 'fs-extra'; import unzipper, { Entry } from 'unzipper'; import archiver from 'archiver'; @@ -131,7 +131,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { const dir = options?.targetDir ?? - (await fs.mkdtemp(path.join(this.workDir, 'backstage-'))); + (await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-'))); await this.stream .pipe(unzipper.Parse()) @@ -140,11 +140,11 @@ export class ZipArchiveResponse implements ReadTreeResponse { // as a zip can have files with directories without directory entries if (entry.type === 'File' && this.shouldBeIncluded(entry)) { const entryPath = this.getPath(entry); - const dirname = path.dirname(entryPath); + const dirname = platformPath.dirname(entryPath); if (dirname) { - await fs.mkdirp(path.join(dir, dirname)); + await fs.mkdirp(platformPath.join(dir, dirname)); } - entry.pipe(fs.createWriteStream(path.join(dir, entryPath))); + entry.pipe(fs.createWriteStream(platformPath.join(dir, entryPath))); } else { entry.autodrain(); } diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 6739d1c2ce..e7fd4cb688 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -38,6 +38,8 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], rules: { + 'no-shadow': 'off', + 'no-redeclare': 'off', '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-redeclare': 'error', diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index c440e53bc0..5c2206f8c1 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -45,6 +45,8 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], rules: { + 'no-shadow': 'off', + 'no-redeclare': 'off', '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-redeclare': 'error', diff --git a/packages/cli/src/commands/config/print.ts b/packages/cli/src/commands/config/print.ts index 56dcd3f753..930f2c98ca 100644 --- a/packages/cli/src/commands/config/print.ts +++ b/packages/cli/src/commands/config/print.ts @@ -63,8 +63,8 @@ function serializeConfigData( } const sanitizedConfigs = schema.process(appConfigs, { - valueTransform: (value, { visibility }) => - visibility === 'secret' ? '' : value, + valueTransform: (value, context) => + context.visibility === 'secret' ? '' : value, }); return ConfigReader.fromConfigs(sanitizedConfigs).get(); diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 987d4f6f31..31803d4827 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -108,7 +108,7 @@ describe('bump', () => { paths.targetDir = '/'; jest .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...paths) => resolvePath('/', ...paths)); + .mockImplementation((...path) => resolvePath('/', ...path)); jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => JSON.stringify({ type: 'inspect', @@ -204,7 +204,7 @@ describe('bump', () => { paths.targetDir = '/'; jest .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...paths) => resolvePath('/', ...paths)); + .mockImplementation((...path) => resolvePath('/', ...path)); jest.spyOn(runObj, 'runPlain').mockImplementation(async () => ''); jest.spyOn(runObj, 'run').mockResolvedValue(undefined); diff --git a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts index 0478337c61..fed8b39a19 100644 --- a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts +++ b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts @@ -41,7 +41,7 @@ export class LinkedPackageResolvePlugin implements ResolvePlugin { callback: () => void, ) => { const pkg = this.packages.find( - pkg => data.path && isChildPath(pkg.location, data.path), + pkge => data.path && isChildPath(pkge.location, data.path), ); if (!pkg) { callback(); diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index f38416ca4e..c8d48a7199 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -42,14 +42,14 @@ export type BundlingPathsOptions = { export function resolveBundlingPaths(options: BundlingPathsOptions) { const { entry } = options; - const resolveTargetModule = (path: string) => { + const resolveTargetModule = (pathString: string) => { for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) { - const filePath = paths.resolveTarget(`${path}.${ext}`); + const filePath = paths.resolveTarget(`${pathString}.${ext}`); if (fs.pathExistsSync(filePath)) { return filePath; } } - return paths.resolveTarget(`${path}.js`); + return paths.resolveTarget(`${pathString}.js`); }; let targetPublic = undefined; diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index 0e53562875..611413931e 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -118,7 +118,9 @@ export async function collectConfigSchemas( } await Promise.all( - depNames.map(name => processItem({ name, parentPath: pkgPath })), + depNames.map(depName => + processItem({ name: depName, parentPath: pkgPath }), + ), ); } diff --git a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts index 517bf82ae7..71671edba6 100644 --- a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -27,11 +27,11 @@ type Options = { provider: AuthProvider & { id: string }; }; -export type DirectAuthResponse = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; +// export type DirectAuthResponse = { +// userId: string; +// profile: ProfileInfo; +// backstageIdentity: BackstageIdentity; +// }; export class DirectAuthConnector { private readonly discoveryApi: DiscoveryApi; diff --git a/packages/core-api/src/routing/FlatRoutes.tsx b/packages/core-api/src/routing/FlatRoutes.tsx index a6783964a7..cefb347290 100644 --- a/packages/core-api/src/routing/FlatRoutes.tsx +++ b/packages/core-api/src/routing/FlatRoutes.tsx @@ -25,8 +25,8 @@ type RouteObject = { // Similar to the same function from react-router, this collects routes from the // children, but only the first level of routes -function createRoutesFromChildren(children: ReactNode): RouteObject[] { - return Children.toArray(children) +function createRoutesFromChildren(childrenNode: ReactNode): RouteObject[] { + return Children.toArray(childrenNode) .flatMap(child => { if (!isValidElement(child)) { return []; diff --git a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx index a1b4e1a97f..ef1f596f83 100644 --- a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx @@ -38,13 +38,13 @@ const useStyles = makeStyles(theme => ({ }, })); -export type Tab = { +export type objectTab = { id: string; label: string; }; type HeaderTabsProps = { - tabs: Tab[]; + tabs: objectTab[]; onChange?: (index: number) => void; selectedIndex?: number; }; diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 2ddcc333d2..5f4894f9e9 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -104,11 +104,11 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { { helpers: { version(name: string) { - const pkg = require(`${name}/package.json`); - if (!pkg) { + const pkge = require(`${name}/package.json`); + if (!pkge) { throw new Error(`No version available for package ${name}`); } - return pkg.version; + return pkge.version; }, }, }, diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index b43e69eba4..69eb6cc367 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -43,10 +43,10 @@ describe('ScmIntegrations', () => { } as GitLabIntegrationConfig); const i = new ScmIntegrations({ - azure: basicIntegrations([azure], i => i.config.host), - bitbucket: basicIntegrations([bitbucket], i => i.config.host), - github: basicIntegrations([github], i => i.config.host), - gitlab: basicIntegrations([gitlab], i => i.config.host), + azure: basicIntegrations([azure], item => item.config.host), + bitbucket: basicIntegrations([bitbucket], item => item.config.host), + github: basicIntegrations([github], item => item.config.host), + gitlab: basicIntegrations([gitlab], item => item.config.host), }); it('can get the specifics', () => { diff --git a/packages/integration/src/github/GithubCredentialsProvider.test.ts b/packages/integration/src/github/GithubCredentialsProvider.test.ts index f708f75184..3eb1d33306 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.test.ts @@ -215,21 +215,21 @@ describe('GithubCredentialsProvider tests', () => { }); it('should return the default token if no app is configured', async () => { - const github = GithubCredentialsProvider.create({ + const githubID = GithubCredentialsProvider.create({ host: 'github.com', apps: [], token: 'fallback_token', }); await expect( - github.getCredentials({ + githubID.getCredentials({ url: 'https://github.com/404/foobar', }), ).resolves.toEqual(expect.objectContaining({ token: 'fallback_token' })); }); it('should return the configured token if listing installations throws', async () => { - const github = GithubCredentialsProvider.create({ + const githubID = GithubCredentialsProvider.create({ host: 'github.com', apps: [ { @@ -245,19 +245,19 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.listInstallations.mockRejectedValue({ status: 304 }); await expect( - github.getCredentials({ + githubID.getCredentials({ url: 'https://github.com/backstage', }), ).resolves.toEqual(expect.objectContaining({ token: 'hardcoded_token' })); }); it('should return undefined if no token or apps are configured', async () => { - const github = GithubCredentialsProvider.create({ + const githubID = GithubCredentialsProvider.create({ host: 'github.com', }); await expect( - github.getCredentials({ + githubID.getCredentials({ url: 'https://github.com/backstage', }), ).resolves.toEqual({ headers: undefined, token: undefined }); diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index dfb809ee5f..a22c69909b 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -177,7 +177,7 @@ export class GithubAppCredentialsMux { ), ); - const result = results.find(result => result.credentials); + const result = results.find(resultItem => resultItem.credentials); if (result) { return result.credentials!.accessToken; } diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts index efd56285ef..d444d25540 100644 --- a/packages/techdocs-common/src/stages/publish/local.test.ts +++ b/packages/techdocs-common/src/stages/publish/local.test.ts @@ -28,9 +28,9 @@ jest.mock('fs-extra', () => { const fsOriginal = jest.requireActual('fs-extra'); return { ...fsOriginal, - access: jest.fn().mockImplementation((path, checkType, callback) => { + access: jest.fn().mockImplementation((paths, checkType, callback) => { if ( - path.includes('http://localhost:7000/static') && + paths.includes('http://localhost:7000/static') && checkType === fs.constants.F_OK ) { callback(); diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index d59699ab9a..19fb886d74 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -74,8 +74,8 @@ paths: type: 'openapi', title: 'OpenAPI', rawLanguage: 'yaml', - component: definition => ( - + component: definitionString => ( + ), }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 76fc63c9b3..b03183cf34 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -124,7 +124,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { // produce tens of thousands of entities, and those are too large batch // sizes to reasonably send to the database. const batches = Object.values(requestsByKindAndNamespace) - .map(requests => chunk(requests, BATCH_SIZE)) + .map(request => chunk(request, BATCH_SIZE)) .flat(); // Bound the number of concurrent batches. We want a bit of concurrency for diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index 6eb7a45c63..28200021cc 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -65,7 +65,10 @@ describe('AwsOrganizationCloudAccountProcessor', () => { }); it('filters out accounts not in specified location target', async () => { - const location = { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3' }; + const locationTest = { + type: 'aws-cloud-accounts', + target: 'o-1vl18kc5a3', + }; listAccounts.mockImplementation(() => Promise.resolve({ Accounts: [ @@ -83,11 +86,11 @@ describe('AwsOrganizationCloudAccountProcessor', () => { NextToken: undefined, }), ); - await processor.readLocation(location, false, emit); + await processor.readLocation(locationTest, false, emit); expect(emit).toBeCalledTimes(1); expect(emit).toBeCalledWith({ type: 'entity', - location, + location: locationTest, entity: { apiVersion: 'backstage.io/v1alpha1', kind: 'Resource', diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index c75a46874d..ef94c54010 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -66,8 +66,8 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { async validateEntityKind(entity: Entity): Promise { for (const validator of this.validators) { - const result = await validator.check(entity); - if (result) { + const results = await validator.check(entity); + if (results) { return true; } } diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index c233be5a8c..0e59fed3c9 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -71,8 +71,8 @@ export const RegisterComponentForm = ({ const onSubmit = async (formData: Record) => { const { componentLocation: target } = formData; - async function saveCatalogFileConfig(target: string) { - const data = await catalogApi.addLocation({ target }); + async function saveCatalogFileConfig(targetString: string) { + const data = await catalogApi.addLocation({ target: targetString }); saveConfig({ type: 'file', location: data.location.target, @@ -80,12 +80,12 @@ export const RegisterComponentForm = ({ }); } - async function trySaveRepositoryConfig(target: string) { - const existingCatalog = await checkForExistingCatalogInfo(target); + async function trySaveRepositoryConfig(targetString: string) { + const existingCatalog = await checkForExistingCatalogInfo(targetString); if (existingCatalog.exists) { - const targetUrl = target.endsWith('/') - ? `${target}${existingCatalog.url}` - : `${target}/${existingCatalog.url}`; + const targetUrl = targetString.endsWith('/') + ? `${targetString}${existingCatalog.url}` + : `${targetString}/${existingCatalog.url}`; await saveCatalogFileConfig(targetUrl); } else { saveConfig({ diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index e284e95456..adae137a50 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -46,20 +46,20 @@ function manifestGenerationAvailable(configApi: ConfigApi): boolean { function repositories(configApi: ConfigApi): string[] { const integrations = configApi.getConfig('integrations'); - const repositories = []; + const repos = []; if (integrations.has('github')) { - repositories.push('GitHub'); + repos.push('GitHub'); } if (integrations.has('bitbucket')) { - repositories.push('Bitbucket'); + repos.push('Bitbucket'); } if (integrations.has('gitlab')) { - repositories.push('GitLab'); + repos.push('GitLab'); } if (integrations.has('azure')) { - repositories.push('Azure'); + repos.push('Azure'); } - return repositories; + return repos; } export const ImportComponentPage = ({ diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 09b8d02c60..7f09cf557b 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -53,8 +53,10 @@ const Route: (props: SubRoute) => null = () => null; // This causes all mount points that are discovered within this route to use the path of the route itself attachComponentData(Route, 'core.gatherMountPoints', true); -export function createSubRoutesFromChildren(children: ReactNode): SubRoute[] { - return Children.toArray(children).flatMap(child => { +export function createSubRoutesFromChildren( + childrenNode: ReactNode, +): SubRoute[] { + return Children.toArray(childrenNode).flatMap(child => { if (!isValidElement(child)) { return []; } diff --git a/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx b/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx index a2209ba492..47f7b12eb3 100644 --- a/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx @@ -52,12 +52,12 @@ export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => { [routes], ); - const onTabChange = (index: number) => + const onTabChange = (tabIndex: number) => // Remove trailing /* // And remove leading / for relative navigation // Note! route resolves relative to the position in the React tree, // not relative to current location - navigate(routes[index].path.replace(/\/\*$/, '').replace(/^\//, '')); + navigate(routes[tabIndex].path.replace(/\/\*$/, '').replace(/^\//, '')); return ( <> diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index f36bc29cee..3333a85d29 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -35,8 +35,8 @@ type SwitchCase = { children: JSX.Element; }; -function createSwitchCasesFromChildren(children: ReactNode): SwitchCase[] { - return Children.toArray(children).flatMap(child => { +function createSwitchCasesFromChildren(childrenNode: ReactNode): SwitchCase[] { + return Children.toArray(childrenNode).flatMap(child => { if (!isValidElement(child)) { return []; } diff --git a/plugins/catalog/src/components/isOwnerOf.ts b/plugins/catalog/src/components/isOwnerOf.ts index 455e06dc13..d135e4030a 100644 --- a/plugins/catalog/src/components/isOwnerOf.ts +++ b/plugins/catalog/src/components/isOwnerOf.ts @@ -34,13 +34,13 @@ export function isOwnerOf(owner: Entity, owned: Entity) { const owners = getEntityRelations(owned, RELATION_OWNED_BY); - for (const owner of owners) { + for (const ownerItem of owners) { if ( possibleOwners.find( o => - owner.kind.toLowerCase() === o.kind.toLowerCase() && - owner.namespace.toLowerCase() === o.namespace.toLowerCase() && - owner.name.toLowerCase() === o.name.toLowerCase(), + ownerItem.kind.toLowerCase() === o.kind.toLowerCase() && + ownerItem.namespace.toLowerCase() === o.namespace.toLowerCase() && + ownerItem.name.toLowerCase() === o.name.toLowerCase(), ) !== undefined ) { return true; diff --git a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx index aaadbed09d..e836c3ef01 100644 --- a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx +++ b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx @@ -18,11 +18,11 @@ import { Paper, Divider } from '@material-ui/core'; import { AlertActionCard } from './AlertActionCard'; import { Alert } from '../../types'; -type AlertActionCardList = { +type AlertActionCardListType = { alerts: Array; }; -export const AlertActionCardList = ({ alerts }: AlertActionCardList) => ( +export const AlertActionCardList = ({ alerts }: AlertActionCardListType) => ( {alerts.map((alert, index) => ( diff --git a/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx b/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx index c4402893f3..71bb08d563 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx @@ -18,7 +18,7 @@ import React, { PropsWithChildren } from 'react'; import { Box, Typography } from '@material-ui/core'; import { useBarChartLabelStyles } from '../../utils/styles'; -type BarChartLabel = { +type BarChartLabelType = { x: number; y: number; height: number; @@ -33,7 +33,7 @@ export const BarChartLabel = ({ width, details, children, -}: PropsWithChildren) => { +}: PropsWithChildren) => { const classes = useBarChartLabelStyles(); const translateX = width * -0.5; diff --git a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx index 11a295965e..9576e74b42 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { ButtonBase } from '@material-ui/core'; import { useBarChartStepperStyles as useStyles } from '../../utils/styles'; -export type BarChartSteps = { +export type BarChartStepsType = { steps: number; activeStep: number; onClick: (index: number) => void; @@ -28,7 +28,7 @@ export const BarChartSteps = ({ steps, activeStep, onClick, -}: BarChartSteps) => { +}: BarChartStepsType) => { const classes = useStyles(); const handleOnClick = (index: number) => ( event: React.MouseEvent, diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx index 0ef35bf04c..381827e4e2 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx @@ -83,28 +83,31 @@ export const ProductInsights = ({ useEffect(() => { async function getAllProductInsights( - group: string, - project: Maybe, - products: Product[], - lastCompleteBillingDate: string, + groupId: string, + projectId: Maybe, + productsId: Product[], + lastCompleteBillingDateId: string, ) { try { dispatchLoadingProducts(true); const responses = await Promise.allSettled( - products.map(product => + productsId.map(product => client.getProductInsights({ - group: group, - project: project, + group: groupId, + project: projectId, product: product.kind, - intervals: intervalsOf(DEFAULT_DURATION, lastCompleteBillingDate), + intervals: intervalsOf( + DEFAULT_DURATION, + lastCompleteBillingDateId, + ), }), ), ).then(settledResponseOf); - const initialStates = initialStatesOf(products, responses).sort( + const initialStatesNow = initialStatesOf(productsId, responses).sort( totalAggregationSort, ); - setStates(initialStates); + setStates(initialStatesNow); } catch (e) { setError(e); } finally { @@ -125,8 +128,8 @@ export const ProductInsights = ({ useEffect( function handleOnLoaded() { if (onceRef.current) { - const products = initialStates.map(state => state.product); - onLoaded(products); + const currentProducts = initialStates.map(state => state.product); + onLoaded(currentProducts); } else { onceRef.current = true; } diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index fc50439aad..143751fa89 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -26,10 +26,10 @@ import { DateAggregation, } from '../types'; import dayjs, { OpUnitType } from 'dayjs'; -import duration from 'dayjs/plugin/duration'; +import durationPlugin from 'dayjs/plugin/duration'; import { inclusiveStartDateOf } from './duration'; -dayjs.extend(duration); +dayjs.extend(durationPlugin); // Used for displaying status colors export function growthOf(ratio: number, amount?: number) { diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index db7c7ddd55..a1ff3a1eef 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -36,17 +36,17 @@ export function useBuilds(owner: string, repo: string, branch?: string) { const { loading, value: builds, retry } = useAsyncRetry(async () => { try { - let builds; + let build; if (branch) { - builds = await api.getLastBuild(`${owner}/${repo}/${branch}`); + build = await api.getLastBuild(`${owner}/${repo}/${branch}`); } else { - builds = await api.getFolder(`${owner}/${repo}`); + build = await api.getFolder(`${owner}/${repo}`); } - const size = Array.isArray(builds) ? builds?.[0].build_num! : 1; + const size = Array.isArray(build) ? build?.[0].build_num! : 1; setTotal(size); - return builds || []; + return build || []; } catch (e) { errorApi.post(e); throw e; diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 2dd1e32b96..811a3c8acb 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -85,18 +85,18 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic ] || `backstage.io/kubernetes-id=${requestBody.entity.metadata.name}`; return Promise.all( - clusterDetailsDecoratedForAuth.map(clusterDetails => { + clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { return fetcher .fetchObjectsForService({ serviceId, - clusterDetails, + clusterDetails: clusterDetailsItem, objectTypesToFetch, labelSelector, } as ObjectFetchParams) .then(result => { return { cluster: { - name: clusterDetails.name, + name: clusterDetailsItem.name, }, resources: result.responses, errors: result.errors, diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx index 6bacad9b16..8c1365769f 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx @@ -32,8 +32,8 @@ export const DeploymentDrawer = ({ object={deployment} expanded={expanded} kind="Deployment" - renderObject={(deployment: V1Deployment) => { - const conditions = (deployment.status?.conditions ?? []) + renderObject={(deploymentObj: V1Deployment) => { + const conditions = (deploymentObj.status?.conditions ?? []) .map(renderCondition) .reduce((accum, next) => { accum[next[0]] = next[1]; @@ -41,10 +41,10 @@ export const DeploymentDrawer = ({ }, {} as { [key: string]: React.ReactNode }); return { - strategy: deployment.spec?.strategy ?? '???', - minReadySeconds: deployment.spec?.minReadySeconds ?? '???', + strategy: deploymentObj.spec?.strategy ?? '???', + minReadySeconds: deploymentObj.spec?.minReadySeconds ?? '???', progressDeadlineSeconds: - deployment.spec?.progressDeadlineSeconds ?? '???', + deploymentObj.spec?.progressDeadlineSeconds ?? '???', ...conditions, }; }} diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx index 00ab2cad84..26dcdfd249 100644 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx @@ -32,16 +32,16 @@ export const HorizontalPodAutoscalerDrawer = ({ kind="HorizontalPodAutoscaler" object={hpa} expanded={expanded} - renderObject={(hpa: V1HorizontalPodAutoscaler) => { + renderObject={(hpaObject: V1HorizontalPodAutoscaler) => { return { targetCPUUtilizationPercentage: - hpa.spec?.targetCPUUtilizationPercentage, + hpaObject.spec?.targetCPUUtilizationPercentage, currentCPUUtilizationPercentage: - hpa.status?.currentCPUUtilizationPercentage, - minReplicas: hpa.spec?.minReplicas, - maxReplicas: hpa.spec?.maxReplicas, - currentReplicas: hpa.status?.currentReplicas, - desiredReplicas: hpa.status?.desiredReplicas, + hpaObject.status?.currentCPUUtilizationPercentage, + minReplicas: hpaObject.spec?.minReplicas, + maxReplicas: hpaObject.spec?.maxReplicas, + currentReplicas: hpaObject.status?.currentReplicas, + desiredReplicas: hpaObject.status?.desiredReplicas, }; }} > diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx index 9e63f22fda..d619700ac2 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx @@ -31,8 +31,8 @@ export const IngressDrawer = ({ object={ingress} expanded={expanded} kind="Ingress" - renderObject={(ingress: ExtensionsV1beta1Ingress) => { - return ingress.spec || {}; + renderObject={(ingressObject: ExtensionsV1beta1Ingress) => { + return ingressObject.spec || {}; }} > { - const phase = pod.status?.phase ?? 'unknown'; + renderObject={(podObject: V1Pod) => { + const phase = podObject.status?.phase ?? 'unknown'; const ports = - pod.spec?.containers?.map(c => { + podObject.spec?.containers?.map(c => { return { [c.name]: c.ports, }; }) ?? 'N/A'; - const conditions = (pod.status?.conditions ?? []) + const conditions = (podObject.status?.conditions ?? []) .map(renderCondition) .reduce((accum, next) => { accum[next[0]] = next[1]; @@ -55,11 +55,11 @@ export const PodDrawer = ({ }, {} as { [key: string]: React.ReactNode }); return { - images: imageChips(pod), + images: imageChips(podObject), phase: phase, - 'Containers Ready': containersReady(pod), - 'Total Restarts': totalRestarts(pod), - 'Container Statuses': containerStatuses(pod), + 'Containers Ready': containersReady(podObject), + 'Total Restarts': totalRestarts(podObject), + 'Container Statuses': containerStatuses(podObject), ...conditions, 'Exposed ports': ports, }; diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx index d48f17813a..d0962c6314 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx @@ -31,8 +31,8 @@ export const ServiceDrawer = ({ object={service} expanded={expanded} kind="Service" - renderObject={(service: V1Service) => { - return service.spec || {}; + renderObject={(serviceObject: V1Service) => { + return serviceObject.spec || {}; }} > { export const containersReady = (pod: V1Pod): string => { const containerStatuses = pod.status?.containerStatuses ?? []; - const containersReady = containerStatuses.filter(cs => cs.ready).length; + const containersReadyItem = containerStatuses.filter(cs => cs.ready).length; - return `${containersReady}/${containerStatuses.length}`; + return `${containersReadyItem}/${containerStatuses.length}`; }; export const totalRestarts = (pod: V1Pod): number => { @@ -47,8 +47,8 @@ export const totalRestarts = (pod: V1Pod): number => { }; export const containerStatuses = (pod: V1Pod): ReactNode => { - const containerStatuses = pod.status?.containerStatuses ?? []; - const errors = containerStatuses.reduce((accum, next) => { + const containerStatusesItem = pod.status?.containerStatuses ?? []; + const errors = containerStatusesItem.reduce((accum, next) => { if (next.state === undefined) { return accum; } diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 779cb1d92d..0bf104bf61 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -73,12 +73,12 @@ export const GroupProfileCard = ({ } = group; const parent = group?.relations ?.filter(r => r.type === RELATION_CHILD_OF) - ?.map(group => group.target.name) + ?.map(groupItem => groupItem.target.name) .toString(); const childrens = group?.relations ?.filter(r => r.type === RELATION_PARENT_OF) - ?.map(group => group.target.name); + ?.map(groupItem => groupItem.target.name); const displayName = profile?.displayName ?? name; diff --git a/plugins/org/src/components/isOwnerOf.ts b/plugins/org/src/components/isOwnerOf.ts index dd7c7d6805..51b3f41ae8 100644 --- a/plugins/org/src/components/isOwnerOf.ts +++ b/plugins/org/src/components/isOwnerOf.ts @@ -38,13 +38,13 @@ export function isOwnerOf(owner: Entity, owned: Entity) { const owners = getEntityRelations(owned, RELATION_OWNED_BY); - for (const owner of owners) { + for (const ownerItem of owners) { if ( possibleOwners.find( o => - owner.kind.toLowerCase() === o.kind.toLowerCase() && - owner.namespace.toLowerCase() === o.namespace.toLowerCase() && - owner.name.toLowerCase() === o.name.toLowerCase(), + ownerItem.kind.toLowerCase() === o.kind.toLowerCase() && + ownerItem.namespace.toLowerCase() === o.namespace.toLowerCase() && + ownerItem.name.toLowerCase() === o.name.toLowerCase(), ) !== undefined ) { return true; diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx index faa480f6e2..6fd036330a 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx @@ -32,11 +32,11 @@ export const EscalationPolicy = ({ policyId }: Props) => { const { value: users, loading, error } = useAsync(async () => { const oncalls = await api.getOnCallByPolicyId(policyId); - const users = oncalls + const usersItem = oncalls .sort((a, b) => a.escalation_level - b.escalation_level) .map(oncall => oncall.user); - return users; + return usersItem; }); if (error) { diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index 4fa88a1b9a..c390ed1f82 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -52,11 +52,11 @@ export const TriggerDialog = ({ const [description, setDescription] = useState(''); const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn( - async (description: string) => + async (descriptions: string) => await api.triggerAlarm({ integrationKey, source: window.location.toString(), - description, + description: descriptions, userName, }), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index ff74fffdd7..d2fdedccf2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -93,12 +93,12 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => { - const preparer = BitbucketPreparer.fromConfig({ + const preparerCheck = BitbucketPreparer.fromConfig({ host: 'bitbucket.org', username: 'fake-user', appPassword: 'fake-password', }); - await preparer.prepare(mockEntity, { logger }); + await preparerCheck.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -128,12 +128,12 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with with token for auth method', async () => { - const preparer = BitbucketPreparer.fromConfig({ + const preparerCheck = BitbucketPreparer.fromConfig({ host: 'bitbucket.org', token: 'fake-token', }); - await preparer.prepare(mockEntity, { logger }); + await preparerCheck.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index b43545f5fc..3221c5d4ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -104,12 +104,12 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = GithubPreparer.fromConfig({ + const preparerCheck = GithubPreparer.fromConfig({ host: 'github.com', token: 'fake-token', }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { + const response = await preparerCheck.prepare(mockEntity, { logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( @@ -118,13 +118,13 @@ describe('GitHubPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = GithubPreparer.fromConfig({ + const preparerCheck = GithubPreparer.fromConfig({ host: 'github.com', token: 'fake-token', }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { + const response = await preparerCheck.prepare(mockEntity, { workingDirectory: '/workDir', logger: getVoidLogger(), }); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 528d42b94f..0d66fbf92b 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -30,7 +30,7 @@ import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; const Form = withTheme(MuiTheme); -type Step = { +type StepType = { schema: JSONSchema; label: string; } & Partial, 'schema'>>; @@ -39,7 +39,7 @@ type Props = { /** * Steps for the form, each contains label and form schema */ - steps: Step[]; + steps: StepType[]; formData: Record; onChange: (e: IChangeEvent) => void; onReset: () => void; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 60f5ad8dec..9b089fae43 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -92,8 +92,8 @@ export const TemplatePage = () => { ); const [jobId, setJobId] = useState(null); - const job = useJobPolling(jobId, async job => { - if (!job.metadata.catalogInfoUrl) { + const job = useJobPolling(jobId, async jobItem => { + if (!jobItem.metadata.catalogInfoUrl) { errorApi.post( new Error(`No catalogInfoUrl returned from the scaffolder`), ); @@ -103,7 +103,9 @@ export const TemplatePage = () => { try { const { entities: [createdEntity], - } = await catalogApi.addLocation({ target: job.metadata.catalogInfoUrl }); + } = await catalogApi.addLocation({ + target: jobItem.metadata.catalogInfoUrl, + }); const resolvedPath = generatePath( `/catalog/${entityRoute.path}`, @@ -122,8 +124,8 @@ export const TemplatePage = () => { const handleCreate = async () => { try { - const jobId = await scaffolderApi.scaffold(templateName, formState); - setJobId(jobId); + const Id = await scaffolderApi.scaffold(templateName, formState); + setJobId(Id); setModalOpen(true); } catch (e) { errorApi.post(e); diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index f81cd4f66d..4b82679fcc 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -59,10 +59,10 @@ type TableHeaderProps = { handleToggleFilters: () => void; }; -type Filters = { - selected: string; - checked: Array; -}; +// type Filters = { +// selected: string; +// checked: Array; +// }; // TODO: move out column to make the search result component more generic const columns: TableColumn[] = [ From c9d6493bef0aa7de4ac36d33336ee24140372754 Mon Sep 17 00:00:00 2001 From: Debajyoti Halder Date: Fri, 29 Jan 2021 12:15:35 +0530 Subject: [PATCH 003/158] Add missing eslint rule --- packages/cli/config/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 5c2206f8c1..37bd7bed94 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -49,7 +49,7 @@ module.exports = { 'no-redeclare': 'off', '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-redeclare': 'error', - + 'no-undef': 'off', 'import/newline-after-import': 'error', 'import/no-duplicates': 'warn', 'import/no-extraneous-dependencies': [ From 877f46c1777f16acf84b3884a2a5c5ab59bf90d5 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 29 Jan 2021 16:15:34 -0500 Subject: [PATCH 004/158] Support custom status codes --- .../reader/components/TechDocsNotFound.test.tsx | 17 +++++++++++++++++ .../src/reader/components/TechDocsNotFound.tsx | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index d3c98fec78..c6562ec8ad 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -41,3 +41,20 @@ describe('', ( expect(rendered.getByTestId('go-back-link')).toBeDefined(); }); }); + +describe('', () => { + it('should render with a custom status code, custom error message and go back link', () => { + const rendered = render( + wrapInTestApp( + , + ), + ); + rendered.getByText(/This is a custom error message/i); + rendered.getByText(/500/i); + rendered.getByText(/Looks like someone dropped the mic!/i); + expect(rendered.getByTestId('go-back-link')).toBeDefined(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx index cdacc1cb7e..04f2106161 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx @@ -19,9 +19,10 @@ import { ErrorPage, useApi, configApiRef } from '@backstage/core'; type Props = { errorMessage?: string; + statusCode?: number; }; -export const TechDocsNotFound = ({ errorMessage }: Props) => { +export const TechDocsNotFound = ({ errorMessage, statusCode }: Props) => { const techdocsBuilder = useApi(configApiRef).getOptionalString( 'techdocs.builder', ); @@ -37,7 +38,7 @@ export const TechDocsNotFound = ({ errorMessage }: Props) => { return ( From cb61bc8cbb3c0d58dcfc4d74aaeeeb38bb6df263 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 29 Jan 2021 16:16:14 -0500 Subject: [PATCH 005/158] Hardcode file not found status --- plugins/techdocs/src/reader/components/Reader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 1264e2b6eb..c9e76ddeeb 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -155,7 +155,7 @@ export const Reader = ({ entityId, onReady }: Props) => { ]); if (error) { - return ; + return ; } return ( From 45de779d5fa75721dd8bb1865c3e63a8ba8c4976 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 29 Jan 2021 16:20:07 -0500 Subject: [PATCH 006/158] Add changeset --- .changeset/green-rabbits-burn.md | 5 +++++ plugins/techdocs/src/api.ts | 24 ++++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 .changeset/green-rabbits-burn.md diff --git a/.changeset/green-rabbits-burn.md b/.changeset/green-rabbits-burn.md new file mode 100644 index 0000000000..5af68c2000 --- /dev/null +++ b/.changeset/green-rabbits-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Enhance API calls to support trapping 500 errors from techdocs-backend diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 3a950aeeb3..5f2cf924d7 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -119,14 +119,22 @@ export class TechDocsStorageApi implements TechDocsStorage { `${url.endsWith('/') ? url : `${url}/`}index.html`, ); - if (request.status === 404) { - let errorMessage = 'Page not found. '; - // path is empty for the home page of an entity's docs site - if (!path) { - errorMessage += - 'This could be because there is no index.md file in the root of the docs directory of this repository.'; - } - throw new Error(errorMessage); + let errorMessage = ''; + switch (request.status) { + case 404: + errorMessage = 'Page not found. '; + // path is empty for the home page of an entity's docs site + if (!path) { + errorMessage += + 'This could be because there is no index.md file in the root of the docs directory of this repository.'; + } + throw new Error(errorMessage); + case 500: + errorMessage = 'Could not generate documentation. '; + throw new Error(errorMessage); + default: + // Do nothing + break; } return request.text(); From 96dbdfbb41a65524411cce4544abd1f64fba4ded Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Sun, 31 Jan 2021 22:02:25 -0500 Subject: [PATCH 007/158] Update error message --- plugins/techdocs/src/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 5f2cf924d7..fc007503fa 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -130,7 +130,8 @@ export class TechDocsStorageApi implements TechDocsStorage { } throw new Error(errorMessage); case 500: - errorMessage = 'Could not generate documentation. '; + errorMessage = + 'Could not generate documentation or an error in the TechDocs backend. '; throw new Error(errorMessage); default: // Do nothing From 9cda5593afcfec038a637e5a7da5a1a14204c202 Mon Sep 17 00:00:00 2001 From: Kaparwan Manoj Date: Tue, 2 Feb 2021 10:38:47 +1100 Subject: [PATCH 008/158] bitbucket own hosted v5.11.1 branchUrl fix and enbaled error tracing for mkdocs process stack trace --- packages/integration/src/bitbucket/core.ts | 5 +++-- packages/techdocs-common/src/stages/generate/helpers.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index f5235d8189..e9a76d1ee0 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -32,10 +32,11 @@ export async function getBitbucketDefaultBranch( const isHosted = resource === 'bitbucket.org'; // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 + // Changed branchUrl to support Atlassian Bitbucket v5.11.1 , which has different branchUrl format const branchUrl = isHosted ? `${config.apiBaseUrl}/repositories/${project}/${repoName}` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; - + : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; + const response = await fetch(branchUrl, getBitbucketRequestOptions(config)); if (!response.ok) { const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index e396157b92..73be0e8ff0 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -133,6 +133,10 @@ export const runCommand = async ({ process.stderr.on('data', stream => { logStream.write(stream); + // added below change to make OS ( child process) exception to the backend logs + const bufferbase64 = Buffer.from(stream, 'base64'); + const textAscii = bufferbase64.toString('ascii'); + console.log(' Process Stderror trace: ', textAscii); }); process.on('error', error => { From 7717c46517ef01bd7aa35c9269df7123d826b323 Mon Sep 17 00:00:00 2001 From: Debajyoti Halder Date: Tue, 2 Feb 2021 16:42:37 +0530 Subject: [PATCH 009/158] Fix variable names of conflicting types --- .../src/lib/AuthConnector/DirectAuthConnector.ts | 14 +------------- packages/core/src/layout/HeaderTabs/HeaderTabs.tsx | 8 ++++---- .../src/github/GithubCredentialsProvider.test.ts | 12 ++++++------ .../src/components/BarChart/BarChartLabel.tsx | 4 ++-- .../src/components/BarChart/BarChartSteps.tsx | 4 ++-- .../components/ProductInsights/ProductInsights.tsx | 10 +++++----- .../MultistepJsonForm/MultistepJsonForm.tsx | 10 +++++----- .../src/components/TemplatePage/TemplatePage.tsx | 4 ++-- .../src/components/SearchResult/SearchResult.tsx | 5 ----- 9 files changed, 27 insertions(+), 44 deletions(-) diff --git a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts index 71671edba6..e7764fcbaa 100644 --- a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -13,12 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - AuthProvider, - ProfileInfo, - BackstageIdentity, - DiscoveryApi, -} from '../../apis/definitions'; +import { AuthProvider, DiscoveryApi } from '../../apis/definitions'; import { showLoginPopup } from '../loginPopup'; type Options = { @@ -26,13 +21,6 @@ type Options = { environment?: string; provider: AuthProvider & { id: string }; }; - -// export type DirectAuthResponse = { -// userId: string; -// profile: ProfileInfo; -// backstageIdentity: BackstageIdentity; -// }; - export class DirectAuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string | undefined; diff --git a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx index ef1f596f83..718a103f00 100644 --- a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx @@ -18,7 +18,7 @@ // This is just a temporary solution to implementing tabs for now import React, { useState, useEffect } from 'react'; -import { makeStyles, Tabs, Tab } from '@material-ui/core'; +import { makeStyles, Tabs, Tab as TabUI } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ tabsWrapper: { @@ -38,13 +38,13 @@ const useStyles = makeStyles(theme => ({ }, })); -export type objectTab = { +export type Tab = { id: string; label: string; }; type HeaderTabsProps = { - tabs: objectTab[]; + tabs: Tab[]; onChange?: (index: number) => void; selectedIndex?: number; }; @@ -81,7 +81,7 @@ export const HeaderTabs = ({ value={selectedTab} > {tabs.map((tab, index) => ( - { }); it('should return the default token if no app is configured', async () => { - const githubID = GithubCredentialsProvider.create({ + const githubProvider = GithubCredentialsProvider.create({ host: 'github.com', apps: [], token: 'fallback_token', }); await expect( - githubID.getCredentials({ + githubProvider.getCredentials({ url: 'https://github.com/404/foobar', }), ).resolves.toEqual(expect.objectContaining({ token: 'fallback_token' })); }); it('should return the configured token if listing installations throws', async () => { - const githubID = GithubCredentialsProvider.create({ + const githubProvider = GithubCredentialsProvider.create({ host: 'github.com', apps: [ { @@ -245,19 +245,19 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.listInstallations.mockRejectedValue({ status: 304 }); await expect( - githubID.getCredentials({ + githubProvider.getCredentials({ url: 'https://github.com/backstage', }), ).resolves.toEqual(expect.objectContaining({ token: 'hardcoded_token' })); }); it('should return undefined if no token or apps are configured', async () => { - const githubID = GithubCredentialsProvider.create({ + const githubProvider = GithubCredentialsProvider.create({ host: 'github.com', }); await expect( - githubID.getCredentials({ + githubProvider.getCredentials({ url: 'https://github.com/backstage', }), ).resolves.toEqual({ headers: undefined, token: undefined }); diff --git a/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx b/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx index 71bb08d563..cd882c6afd 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartLabel.tsx @@ -18,7 +18,7 @@ import React, { PropsWithChildren } from 'react'; import { Box, Typography } from '@material-ui/core'; import { useBarChartLabelStyles } from '../../utils/styles'; -type BarChartLabelType = { +type BarChartLabelObject = { x: number; y: number; height: number; @@ -33,7 +33,7 @@ export const BarChartLabel = ({ width, details, children, -}: PropsWithChildren) => { +}: PropsWithChildren) => { const classes = useBarChartLabelStyles(); const translateX = width * -0.5; diff --git a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx index 9576e74b42..ec573da68f 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { ButtonBase } from '@material-ui/core'; import { useBarChartStepperStyles as useStyles } from '../../utils/styles'; -export type BarChartStepsType = { +export type BarChartStepsObject = { steps: number; activeStep: number; onClick: (index: number) => void; @@ -28,7 +28,7 @@ export const BarChartSteps = ({ steps, activeStep, onClick, -}: BarChartStepsType) => { +}: BarChartStepsObject) => { const classes = useStyles(); const handleOnClick = (index: number) => ( event: React.MouseEvent, diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx index 381827e4e2..b13376d24a 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx @@ -85,26 +85,26 @@ export const ProductInsights = ({ async function getAllProductInsights( groupId: string, projectId: Maybe, - productsId: Product[], - lastCompleteBillingDateId: string, + productIds: Product[], + lastCompleteBillingDateString: string, ) { try { dispatchLoadingProducts(true); const responses = await Promise.allSettled( - productsId.map(product => + productIds.map(product => client.getProductInsights({ group: groupId, project: projectId, product: product.kind, intervals: intervalsOf( DEFAULT_DURATION, - lastCompleteBillingDateId, + lastCompleteBillingDateString, ), }), ), ).then(settledResponseOf); - const initialStatesNow = initialStatesOf(productsId, responses).sort( + const initialStatesNow = initialStatesOf(productIds, responses).sort( totalAggregationSort, ); setStates(initialStatesNow); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 0d66fbf92b..27c5b4f18a 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -19,7 +19,7 @@ import { Box, Button, Paper, - Step, + Step as StepUI, StepContent, StepLabel, Stepper, @@ -30,7 +30,7 @@ import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; const Form = withTheme(MuiTheme); -type StepType = { +type Step = { schema: JSONSchema; label: string; } & Partial, 'schema'>>; @@ -39,7 +39,7 @@ type Props = { /** * Steps for the form, each contains label and form schema */ - steps: StepType[]; + steps: Step[]; formData: Record; onChange: (e: IChangeEvent) => void; onReset: () => void; @@ -67,7 +67,7 @@ export const MultistepJsonForm = ({ <> {steps.map(({ label, schema, ...formProps }) => ( - + {label}
-
+ ))}
{activeStep === steps.length && ( diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 9b089fae43..e385f9adc3 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -124,8 +124,8 @@ export const TemplatePage = () => { const handleCreate = async () => { try { - const Id = await scaffolderApi.scaffold(templateName, formState); - setJobId(Id); + const id = await scaffolderApi.scaffold(templateName, formState); + setJobId(id); setModalOpen(true); } catch (e) { errorApi.post(e); diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 4b82679fcc..dc4a502515 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -59,11 +59,6 @@ type TableHeaderProps = { handleToggleFilters: () => void; }; -// type Filters = { -// selected: string; -// checked: Array; -// }; - // TODO: move out column to make the search result component more generic const columns: TableColumn[] = [ { From 786aa9ae3ac8dde2f2097fd36f1f18b96e37176f Mon Sep 17 00:00:00 2001 From: Manoj - Date: Tue, 2 Feb 2021 22:59:51 +1100 Subject: [PATCH 010/158] branchUrl backward compatibility if 404 with new format --- packages/integration/src/bitbucket/core.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index e9a76d1ee0..f22c727f63 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -32,12 +32,19 @@ export async function getBitbucketDefaultBranch( const isHosted = resource === 'bitbucket.org'; // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 - // Changed branchUrl to support Atlassian Bitbucket v5.11.1 , which has different branchUrl format const branchUrl = isHosted ? `${config.apiBaseUrl}/repositories/${project}/${repoName}` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; + : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; - const response = await fetch(branchUrl, getBitbucketRequestOptions(config)); + var response = await fetch(branchUrl, getBitbucketRequestOptions(config)); + + if (response.status === 404) { + // First try the new format, and then if it gets specifically a 404 it should try the old format + // (to support old Atlassian Bitbucket v5.11.1 format ) + branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; + response = await fetch(branchUrl, getBitbucketRequestOptions(config)); + } + if (!response.ok) { const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; throw new Error(message); From 07f86e88ffaf06d31a289a105bde52b8d225ab9c Mon Sep 17 00:00:00 2001 From: Manoj - Date: Tue, 2 Feb 2021 23:02:31 +1100 Subject: [PATCH 011/158] removed logging statements ( used for debug in local earlier) --- packages/techdocs-common/src/stages/generate/helpers.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 73be0e8ff0..e396157b92 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -133,10 +133,6 @@ export const runCommand = async ({ process.stderr.on('data', stream => { logStream.write(stream); - // added below change to make OS ( child process) exception to the backend logs - const bufferbase64 = Buffer.from(stream, 'base64'); - const textAscii = bufferbase64.toString('ascii'); - console.log(' Process Stderror trace: ', textAscii); }); process.on('error', error => { From be8fc669fbdbf99866ca42abd93ee0b2b1b18aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 2 Feb 2021 13:33:31 +0100 Subject: [PATCH 012/158] backend-common: bump UrlReader integration test timeout --- packages/backend-common/src/reading/integration.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend-common/src/reading/integration.test.ts b/packages/backend-common/src/reading/integration.test.ts index da2e916210..acac059117 100644 --- a/packages/backend-common/src/reading/integration.test.ts +++ b/packages/backend-common/src/reading/integration.test.ts @@ -69,6 +69,8 @@ function withRetries(count: number, fn: () => Promise) { } describe('UrlReaders', () => { + jest.setTimeout(30_000); + it( 'should read data from azure', withRetries(3, async () => { From 21e18aced4a1392053407dfab0fca997bdb50bf3 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 17:18:07 +0100 Subject: [PATCH 013/158] Create inital class structure --- .../src/scaffolder/tasks/Database.ts | 35 +++++++++++++++++++ .../src/scaffolder/tasks/TaskObserver.ts | 15 ++++++++ .../src/scaffolder/tasks/taskWorker.ts | 34 ++++++++++++++++++ .../src/scaffolder/tasks/tasksDispatcher.ts | 29 +++++++++++++++ .../src/scaffolder/tasks/types.ts | 30 ++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/types.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts new file mode 100644 index 0000000000..f446f1e90a --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Task, Status } from './types'; + +export class Database { + private readonly store = new Map(); + + get(taskId: string) { + return this.store.get(taskId); + } + + write(task: Task) { + return task; + } + + writeStatus(taskId: string, status: Status) { + const task = this.store.get(taskId); + if (task) { + this.store.set(taskId, { ...task, status }); + } + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts new file mode 100644 index 0000000000..863d6e76e1 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts @@ -0,0 +1,15 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts new file mode 100644 index 0000000000..4b1754523c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Status, ClaimResponse } from './types'; +import { Database } from './Database'; + +export class TaskWorker { + static async fromConfig() {} + + constructor(private readonly database: Database) {} + + claim(): Promise { + return Promise.resolve(undefined); + } + + setStatus(taskId: string, status: Status) { + this.database.writeStatus(taskId, status); + } + + heartbeat(runId: number) {} +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts new file mode 100644 index 0000000000..c3dfaa3c88 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Task } from './types'; +import { Database } from './Database'; + +export class taskDispatcher { + static async fromConfig(config: { database: Database }) {} + + constructor(private readonly db: Database) {} + + dispatch(task: Task): Promise { + this.db.write(task); + return Promise.resolve('uuid'); + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts new file mode 100644 index 0000000000..000e292e16 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Status = 'OPEN' | 'PROCESSING' | 'FAILED' | 'CANCELLED'; +export type Task = { + taskId: string; + metadata: string; + status: Status; + lastHeartbeat: string; + retryCount: number; + createdAt: string; +}; + +export type ClaimResponse = { + runId: number; + task: Task; +}; From 5e06a9f45b65f26b971ae7101dc2c2f709a4bba5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Jan 2021 15:53:01 +0100 Subject: [PATCH 014/158] Refactor into broker --- plugins/scaffolder-backend/package.json | 2 + .../src/scaffolder/tasks/Database.ts | 74 ++++++++++-- .../src/scaffolder/tasks/TaskBroker.test.ts | 63 ++++++++++ .../src/scaffolder/tasks/TaskBroker.ts | 109 ++++++++++++++++++ .../src/scaffolder/tasks/TaskObserver.ts | 15 --- .../src/scaffolder/tasks/taskWorker.ts | 34 ------ .../src/scaffolder/tasks/tasksDispatcher.ts | 29 ----- .../src/scaffolder/tasks/types.ts | 40 +++++-- 8 files changed, 272 insertions(+), 94 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7af9f567e3..97ab723e81 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -53,7 +53,9 @@ "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", + "luxon": "^1.25.0", "morgan": "^1.10.0", + "p-queue": "^6.3.0", "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0" diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts index f446f1e90a..7c3f343b0f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts @@ -13,23 +13,79 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Task, Status } from './types'; +import { DbTaskRow, TaskSpec } from './types'; +import { v4 as uuid } from 'uuid'; +import { DateTime } from 'luxon'; -export class Database { - private readonly store = new Map(); +export interface Database { + get(taskId: string): Promise; + // updateTask(task: Task): Promise; + createTask(task: TaskSpec): Promise; + claimTask(): Promise; + heartBeat(runId: string): Promise; +} - get(taskId: string) { - return this.store.get(taskId); +export class InMemoryDatabase implements Database { + private readonly store = new Map(); + + async heartBeat(runId: string): Promise { + let task: DbTaskRow | undefined; + + for (const t of this.store.values()) { + if (t.runId === runId) { + task = t; + } + } + + if (!task) { + throw new Error('No task with matching runId found'); + } + + this.store.set(task.taskId, { + ...task, + lastHeartbeat: DateTime.local().toString(), + }); } - write(task: Task) { - return task; + async claimTask(): Promise { + for (const t of this.store.values()) { + if (t.status === 'OPEN') { + const task: DbTaskRow = { + ...t, + status: 'PROCESSING', + runId: uuid(), + }; + this.store.set(t.taskId, task); + return task; + } + } + throw new Error('No task found'); } - writeStatus(taskId: string, status: Status) { + async createTask(spec: TaskSpec): Promise { + return { + taskId: uuid(), + spec, + status: 'OPEN', + retryCount: 0, + createdAt: new Date().toISOString(), + }; + } + + async get(taskId: string): Promise { const task = this.store.get(taskId); if (task) { - this.store.set(taskId, { ...task, status }); + return task; } + throw new Error(`could not found task ${taskId}`); } + + // async updateTask(task: Task): Promise { + // if (!task.taskId) { + // throw new Error('Task must contain id'); + // } + + // this.store.set(task.taskId, task); + // return task; + // } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts new file mode 100644 index 0000000000..4fb7439bf8 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { MemoryTaskBroker, TaskAgent } from './TaskBroker'; + +describe('MemoryTaskBroker', () => { + it('should claim a dispatched work item', async () => { + const broker = new MemoryTaskBroker(); + + await broker.dispatch({}); + await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); + }); + + it('should wait for a dispatched work item', async () => { + const broker = new MemoryTaskBroker(); + + const promise = broker.claim(); + + await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); + + await broker.dispatch({}); + await expect(promise).resolves.toEqual(expect.any(TaskAgent)); + }); + + it('should dispatch multiple items and claim them in order', async () => { + const broker = new MemoryTaskBroker(); + + await broker.dispatch({ name: 'a' }); + await broker.dispatch({ name: 'b' }); + await broker.dispatch({ name: 'c' }); + + const taskA = await broker.claim(); + const taskB = await broker.claim(); + const taskC = await broker.claim(); + await expect(taskA).toEqual(expect.any(TaskAgent)); + await expect(taskB).toEqual(expect.any(TaskAgent)); + await expect(taskC).toEqual(expect.any(TaskAgent)); + await expect(taskA.spec.name).toBe('a'); + await expect(taskB.spec.name).toBe('b'); + await expect(taskC.spec.name).toBe('c'); + }); + + it('should complete a task', async () => { + const broker = new MemoryTaskBroker(); + + await broker.dispatch({}); + const task = await broker.claim(); + await task.complete('COMPLETED'); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts new file mode 100644 index 0000000000..322e669373 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + CompletedTaskState, + Task, + TaskSpec, + TaskBroker, + Status, +} from './types'; +import { v4 as uuid } from 'uuid'; +import { InMemoryDatabase } from './database'; + +export class TaskAgent implements Task { + private heartbeartInterval?: ReturnType; + + static create(state: TaskState, db: InMemoryDatabase) { + const agent = new TaskAgent(state, db); + agent.start(); + return agent; + } + + // Runs heartbeat internally + private constructor( + private readonly state: TaskState, + private readonly db: InMemoryDatabase, + ) {} + + get spec() { + return this.state.spec; + } + + async emitLog(message: string): Promise { + throw new Error('Method not implemented.'); + } + + async complete(result: CompletedTaskState): Promise { + this.state.status = result === 'FAILED' ? 'COMPLETED' : 'FAILED'; + } + + private start() { + this.heartbeartInterval = setInterval(() => { + const runId = 'iiiid'; + this.db.heartBeat(runId); + }, 4269); + } +} + +interface TaskState { + spec: TaskSpec; + status: Status; + runId: string | undefined; +} + +function defer() { + let resolve = () => {}; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve }; +} + +export class MemoryTaskBroker implements TaskBroker { + private readonly db = new InMemoryDatabase(); + private readonly tasks = new Array(); + private deferredDispatch = defer(); + + async claim(): Promise { + for (;;) { + const pendingTask = await this.db.claimTask(); + if (pendingTask) { + return TaskAgent.create(pendingTask, this.db); + } + + await this.waitForDispatch(); + } + } + + async dispatch(spec: TaskSpec): Promise { + this.tasks.push({ + spec, + status: 'OPEN', + runId: undefined, + }); + this.signalDispatch(); + } + + private waitForDispatch() { + return this.deferredDispatch.promise; + } + + private signalDispatch() { + this.deferredDispatch.resolve(); + this.deferredDispatch = defer(); + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts deleted file mode 100644 index 863d6e76e1..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskObserver.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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. - */ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts deleted file mode 100644 index 4b1754523c..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/taskWorker.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { Status, ClaimResponse } from './types'; -import { Database } from './Database'; - -export class TaskWorker { - static async fromConfig() {} - - constructor(private readonly database: Database) {} - - claim(): Promise { - return Promise.resolve(undefined); - } - - setStatus(taskId: string, status: Status) { - this.database.writeStatus(taskId, status); - } - - heartbeat(runId: number) {} -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts deleted file mode 100644 index c3dfaa3c88..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/tasksDispatcher.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { Task } from './types'; -import { Database } from './Database'; - -export class taskDispatcher { - static async fromConfig(config: { database: Database }) {} - - constructor(private readonly db: Database) {} - - dispatch(task: Task): Promise { - this.db.write(task); - return Promise.resolve('uuid'); - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 000e292e16..bd8bb0ade5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,17 +14,43 @@ * limitations under the License. */ -export type Status = 'OPEN' | 'PROCESSING' | 'FAILED' | 'CANCELLED'; -export type Task = { +export type Status = + | 'OPEN' + | 'PROCESSING' + | 'FAILED' + | 'CANCELLED' + | 'COMPLETED'; + +export type CompletedTaskState = 'FAILED' | 'COMPLETED'; + +export type DbTaskRow = { taskId: string; - metadata: string; + spec: TaskSpec; status: Status; - lastHeartbeat: string; + lastHeartbeat?: string; retryCount: number; createdAt: string; + runId?: string; +}; + +export type DbTaskEventRow = { + id: number; + runId?: number; + stageName: string; + createdAt: string; }; -export type ClaimResponse = { - runId: number; - task: Task; +export type TaskSpec = { + metadata: string; }; + +export interface Task { + spec: TaskSpec; + emitLog(message: string): Promise; + complete(result: CompletedTaskState): Promise; +} + +export interface TaskBroker { + claim(): Promise; + dispatch(spec: TaskSpec): Promise; +} From 5958318b322cba9c95464eadd5d617cde838d62b Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 09:33:30 +0100 Subject: [PATCH 015/158] Implement createTask and setStatus --- .../src/scaffolder/tasks/Database.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts index 7c3f343b0f..a3ce732586 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DbTaskRow, TaskSpec } from './types'; +import { DbTaskRow, Status, TaskSpec } from './types'; import { v4 as uuid } from 'uuid'; import { DateTime } from 'luxon'; export interface Database { get(taskId: string): Promise; - // updateTask(task: Task): Promise; createTask(task: TaskSpec): Promise; claimTask(): Promise; heartBeat(runId: string): Promise; + setStatus(taskId: string, status: Status): Promise; } export class InMemoryDatabase implements Database { @@ -63,13 +63,15 @@ export class InMemoryDatabase implements Database { } async createTask(spec: TaskSpec): Promise { - return { + const taskRow = { taskId: uuid(), spec, - status: 'OPEN', + status: 'OPEN' as Status, retryCount: 0, createdAt: new Date().toISOString(), }; + this.store.set(taskRow.taskId, taskRow); + return taskRow; } async get(taskId: string): Promise { @@ -80,12 +82,11 @@ export class InMemoryDatabase implements Database { throw new Error(`could not found task ${taskId}`); } - // async updateTask(task: Task): Promise { - // if (!task.taskId) { - // throw new Error('Task must contain id'); - // } - - // this.store.set(task.taskId, task); - // return task; - // } + async setStatus(taskId: string, status: Status): Promise { + const task = this.store.get(taskId); + if (!task) { + throw new Error(`no task found`); + } + this.store.set(task.taskId, { ...task, status }); + } } From a003fffbbb52a898166a91054a95f4ed51146ec6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 09:34:05 +0100 Subject: [PATCH 016/158] Use InMemoryDatabase --- .../src/scaffolder/tasks/TaskBroker.test.ts | 18 +++++---- .../src/scaffolder/tasks/TaskBroker.ts | 40 +++++++++---------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts index 4fb7439bf8..ef4b60cfd6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts @@ -20,7 +20,9 @@ describe('MemoryTaskBroker', () => { it('should claim a dispatched work item', async () => { const broker = new MemoryTaskBroker(); - await broker.dispatch({}); + await broker.dispatch({ + metadata: '', + }); await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); }); @@ -31,16 +33,16 @@ describe('MemoryTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); - await broker.dispatch({}); + await broker.dispatch({ metadata: 'foo' }); await expect(promise).resolves.toEqual(expect.any(TaskAgent)); }); it('should dispatch multiple items and claim them in order', async () => { const broker = new MemoryTaskBroker(); - await broker.dispatch({ name: 'a' }); - await broker.dispatch({ name: 'b' }); - await broker.dispatch({ name: 'c' }); + await broker.dispatch({ metadata: 'a' }); + await broker.dispatch({ metadata: 'b' }); + await broker.dispatch({ metadata: 'c' }); const taskA = await broker.claim(); const taskB = await broker.claim(); @@ -48,9 +50,9 @@ describe('MemoryTaskBroker', () => { await expect(taskA).toEqual(expect.any(TaskAgent)); await expect(taskB).toEqual(expect.any(TaskAgent)); await expect(taskC).toEqual(expect.any(TaskAgent)); - await expect(taskA.spec.name).toBe('a'); - await expect(taskB.spec.name).toBe('b'); - await expect(taskC.spec.name).toBe('c'); + await expect(taskA.spec.metadata).toBe('a'); + await expect(taskB.spec.metadata).toBe('b'); + await expect(taskC.spec.metadata).toBe('c'); }); it('should complete a task', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts index 322e669373..c6fb56b42a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - CompletedTaskState, - Task, - TaskSpec, - TaskBroker, - Status, -} from './types'; -import { v4 as uuid } from 'uuid'; +import { CompletedTaskState, Task, TaskSpec, TaskBroker } from './types'; import { InMemoryDatabase } from './database'; export class TaskAgent implements Task { @@ -48,20 +41,25 @@ export class TaskAgent implements Task { } async complete(result: CompletedTaskState): Promise { - this.state.status = result === 'FAILED' ? 'COMPLETED' : 'FAILED'; + this.db.setStatus( + this.state.taskId, + result === 'FAILED' ? 'COMPLETED' : 'FAILED', + ); } private start() { this.heartbeartInterval = setInterval(() => { - const runId = 'iiiid'; - this.db.heartBeat(runId); - }, 4269); + if (!this.state.runId) { + throw new Error('no run id provided'); + } + this.db.heartBeat(this.state.runId); + }, 1000); } } interface TaskState { spec: TaskSpec; - status: Status; + taskId: string; runId: string | undefined; } @@ -75,14 +73,20 @@ function defer() { export class MemoryTaskBroker implements TaskBroker { private readonly db = new InMemoryDatabase(); - private readonly tasks = new Array(); private deferredDispatch = defer(); async claim(): Promise { for (;;) { const pendingTask = await this.db.claimTask(); if (pendingTask) { - return TaskAgent.create(pendingTask, this.db); + return TaskAgent.create( + { + runId: pendingTask.runId, + taskId: pendingTask.taskId, + spec: pendingTask.spec, + }, + this.db, + ); } await this.waitForDispatch(); @@ -90,11 +94,7 @@ export class MemoryTaskBroker implements TaskBroker { } async dispatch(spec: TaskSpec): Promise { - this.tasks.push({ - spec, - status: 'OPEN', - runId: undefined, - }); + await this.db.createTask(spec); this.signalDispatch(); } From 971742b6cbdcf5e020cc9f76ebc4f8c66e76ccab Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 11:21:51 +0100 Subject: [PATCH 017/158] return undefined if no task is found --- plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts index a3ce732586..6f49a1f577 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts @@ -20,7 +20,7 @@ import { DateTime } from 'luxon'; export interface Database { get(taskId: string): Promise; createTask(task: TaskSpec): Promise; - claimTask(): Promise; + claimTask(): Promise; heartBeat(runId: string): Promise; setStatus(taskId: string, status: Status): Promise; } @@ -47,7 +47,7 @@ export class InMemoryDatabase implements Database { }); } - async claimTask(): Promise { + async claimTask(): Promise { for (const t of this.store.values()) { if (t.status === 'OPEN') { const task: DbTaskRow = { @@ -59,7 +59,7 @@ export class InMemoryDatabase implements Database { return task; } } - throw new Error('No task found'); + return undefined; } async createTask(spec: TaskSpec): Promise { From fcbfc56be8fcc5417219960a5a64e854151093d4 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 11:50:09 +0100 Subject: [PATCH 018/158] Add dispatchResult MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .../src/scaffolder/tasks/TaskBroker.test.ts | 24 ++++++++----- .../src/scaffolder/tasks/TaskBroker.ts | 35 ++++++++++++------- .../src/scaffolder/tasks/types.ts | 6 +++- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts index ef4b60cfd6..388e2fdec8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import { InMemoryDatabase } from './Database'; import { MemoryTaskBroker, TaskAgent } from './TaskBroker'; describe('MemoryTaskBroker', () => { - it('should claim a dispatched work item', async () => { - const broker = new MemoryTaskBroker(); + const storage = new InMemoryDatabase(); + const broker = new MemoryTaskBroker(storage); + it('should claim a dispatched work item', async () => { await broker.dispatch({ metadata: '', }); @@ -27,8 +29,6 @@ describe('MemoryTaskBroker', () => { }); it('should wait for a dispatched work item', async () => { - const broker = new MemoryTaskBroker(); - const promise = broker.claim(); await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); @@ -38,8 +38,6 @@ describe('MemoryTaskBroker', () => { }); it('should dispatch multiple items and claim them in order', async () => { - const broker = new MemoryTaskBroker(); - await broker.dispatch({ metadata: 'a' }); await broker.dispatch({ metadata: 'b' }); await broker.dispatch({ metadata: 'c' }); @@ -56,10 +54,18 @@ describe('MemoryTaskBroker', () => { }); it('should complete a task', async () => { - const broker = new MemoryTaskBroker(); - - await broker.dispatch({}); + const dispatchResult = await broker.dispatch({ metadata: 'foo' }); const task = await broker.claim(); await task.complete('COMPLETED'); + const taskRow = await storage.get(dispatchResult.taskId); + expect(taskRow.status).toBe('COMPLETED'); + }); + + it('should fail a task', async () => { + const dispatchResult = await broker.dispatch({ metadata: 'foo' }); + const task = await broker.claim(); + await task.complete('FAILED'); + const taskRow = await storage.get(dispatchResult.taskId); + expect(taskRow.status).toBe('FAILED'); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts index c6fb56b42a..6d6fb0a72f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts @@ -14,14 +14,20 @@ * limitations under the License. */ -import { CompletedTaskState, Task, TaskSpec, TaskBroker } from './types'; -import { InMemoryDatabase } from './database'; +import { + CompletedTaskState, + Task, + TaskSpec, + TaskBroker, + DispatchResult, +} from './types'; +import { InMemoryDatabase } from './Database'; export class TaskAgent implements Task { private heartbeartInterval?: ReturnType; - static create(state: TaskState, db: InMemoryDatabase) { - const agent = new TaskAgent(state, db); + static create(state: TaskState, storage: InMemoryDatabase) { + const agent = new TaskAgent(state, storage); agent.start(); return agent; } @@ -29,7 +35,7 @@ export class TaskAgent implements Task { // Runs heartbeat internally private constructor( private readonly state: TaskState, - private readonly db: InMemoryDatabase, + private readonly storage: InMemoryDatabase, ) {} get spec() { @@ -41,9 +47,9 @@ export class TaskAgent implements Task { } async complete(result: CompletedTaskState): Promise { - this.db.setStatus( + this.storage.setStatus( this.state.taskId, - result === 'FAILED' ? 'COMPLETED' : 'FAILED', + result === 'FAILED' ? 'FAILED' : 'COMPLETED', ); } @@ -52,7 +58,7 @@ export class TaskAgent implements Task { if (!this.state.runId) { throw new Error('no run id provided'); } - this.db.heartBeat(this.state.runId); + this.storage.heartBeat(this.state.runId); }, 1000); } } @@ -72,12 +78,12 @@ function defer() { } export class MemoryTaskBroker implements TaskBroker { - private readonly db = new InMemoryDatabase(); + constructor(private readonly storage: InMemoryDatabase) {} private deferredDispatch = defer(); async claim(): Promise { for (;;) { - const pendingTask = await this.db.claimTask(); + const pendingTask = await this.storage.claimTask(); if (pendingTask) { return TaskAgent.create( { @@ -85,7 +91,7 @@ export class MemoryTaskBroker implements TaskBroker { taskId: pendingTask.taskId, spec: pendingTask.spec, }, - this.db, + this.storage, ); } @@ -93,9 +99,12 @@ export class MemoryTaskBroker implements TaskBroker { } } - async dispatch(spec: TaskSpec): Promise { - await this.db.createTask(spec); + async dispatch(spec: TaskSpec): Promise { + const taskRow = await this.storage.createTask(spec); this.signalDispatch(); + return { + taskId: taskRow.taskId, + }; } private waitForDispatch() { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index bd8bb0ade5..e8cea06005 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -44,6 +44,10 @@ export type TaskSpec = { metadata: string; }; +export type DispatchResult = { + taskId: string; +}; + export interface Task { spec: TaskSpec; emitLog(message: string): Promise; @@ -52,5 +56,5 @@ export interface Task { export interface TaskBroker { claim(): Promise; - dispatch(spec: TaskSpec): Promise; + dispatch(spec: TaskSpec): Promise; } From 06da4425e0ecb46920ad95c700864c15f4eda4ba Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 11:58:59 +0100 Subject: [PATCH 019/158] Prepend broker files with Memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .../src/scaffolder/tasks/{Database.ts => MemoryDatabase.ts} | 2 +- .../tasks/{TaskBroker.test.ts => MemoryTaskBroker.test.ts} | 6 +++--- .../scaffolder/tasks/{TaskBroker.ts => MemoryTaskBroker.ts} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename plugins/scaffolder-backend/src/scaffolder/tasks/{Database.ts => MemoryDatabase.ts} (97%) rename plugins/scaffolder-backend/src/scaffolder/tasks/{TaskBroker.test.ts => MemoryTaskBroker.test.ts} (93%) rename plugins/scaffolder-backend/src/scaffolder/tasks/{TaskBroker.ts => MemoryTaskBroker.ts} (94%) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts similarity index 97% rename from plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts index 6f49a1f577..65e64bf2a8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/Database.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts @@ -25,7 +25,7 @@ export interface Database { setStatus(taskId: string, status: Status): Promise; } -export class InMemoryDatabase implements Database { +export class MemoryDatabase implements Database { private readonly store = new Map(); async heartBeat(runId: string): Promise { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts similarity index 93% rename from plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts index 388e2fdec8..f1493ab3d0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { InMemoryDatabase } from './Database'; -import { MemoryTaskBroker, TaskAgent } from './TaskBroker'; +import { MemoryDatabase } from './MemoryDatabase'; +import { MemoryTaskBroker, TaskAgent } from './MemoryTaskBroker'; describe('MemoryTaskBroker', () => { - const storage = new InMemoryDatabase(); + const storage = new MemoryDatabase(); const broker = new MemoryTaskBroker(storage); it('should claim a dispatched work item', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts similarity index 94% rename from plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts index 6d6fb0a72f..d29aeb9be6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts @@ -21,12 +21,12 @@ import { TaskBroker, DispatchResult, } from './types'; -import { InMemoryDatabase } from './Database'; +import { MemoryDatabase } from './MemoryDatabase'; export class TaskAgent implements Task { private heartbeartInterval?: ReturnType; - static create(state: TaskState, storage: InMemoryDatabase) { + static create(state: TaskState, storage: MemoryDatabase) { const agent = new TaskAgent(state, storage); agent.start(); return agent; @@ -35,7 +35,7 @@ export class TaskAgent implements Task { // Runs heartbeat internally private constructor( private readonly state: TaskState, - private readonly storage: InMemoryDatabase, + private readonly storage: MemoryDatabase, ) {} get spec() { From 4f67e8aa21c9d1455137990a493614731d7d7333 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 12:51:05 +0100 Subject: [PATCH 020/158] Fix typo --- .../scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts index d29aeb9be6..a5d90898f5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts @@ -78,7 +78,7 @@ function defer() { } export class MemoryTaskBroker implements TaskBroker { - constructor(private readonly storage: InMemoryDatabase) {} + constructor(private readonly storage: MemoryDatabase) {} private deferredDispatch = defer(); async claim(): Promise { From 1c1ff59ebac9a8b150da6d47a1e07ac1a8f860d8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Jan 2021 16:58:41 +0100 Subject: [PATCH 021/158] Add new apis using task broker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .../scaffolder/tasks/MemoryDatabase.test.ts | 21 ++++ .../src/scaffolder/tasks/MemoryDatabase.ts | 50 +++++++- .../src/scaffolder/tasks/MemoryTaskBroker.ts | 54 ++++++++- .../src/scaffolder/tasks/TaskWorker.ts | 110 ++++++++++++++++++ .../src/scaffolder/tasks/index.ts | 19 +++ .../src/scaffolder/tasks/types.ts | 11 +- .../scaffolder-backend/src/service/router.ts | 73 ++++++++++++ 7 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/index.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.test.ts new file mode 100644 index 0000000000..c5522cb38a --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ + +describe('MemoryDatabase', () => { + it('should be tested', async () => { + expect(1).toBe(2); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts index 65e64bf2a8..69fb6a1567 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts @@ -13,22 +13,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DbTaskRow, Status, TaskSpec } from './types'; + +import { DbTaskRow, DbTaskEventRow, Status, TaskSpec } from './types'; import { v4 as uuid } from 'uuid'; -import { DateTime } from 'luxon'; export interface Database { get(taskId: string): Promise; createTask(task: TaskSpec): Promise; claimTask(): Promise; - heartBeat(runId: string): Promise; + heartbeat(runId: string): Promise; setStatus(taskId: string, status: Status): Promise; } +type EmitOptions = { + taskId: string; + runId: string; + event: string; +}; + +type ReadOptions = { + taskId: string; + after?: number | undefined; +}; + export class MemoryDatabase implements Database { private readonly store = new Map(); + private readonly events = new Array(); - async heartBeat(runId: string): Promise { + async emit({ taskId, runId, event }: EmitOptions) { + this.events.push({ + id: this.events.length, + taskId, + runId, + event, + createdAt: new Date().toISOString(), + }); + } + + async getEvents({ + taskId, + after, + }: ReadOptions): Promise<{ events: DbTaskEventRow[] }> { + const events = this.events.filter(event => { + if (event.taskId !== taskId) { + return false; + } + if (after !== undefined) { + if (event.id <= after) { + return false; + } + } + return true; + }); + return { events }; + } + + async heartbeat(runId: string): Promise { let task: DbTaskRow | undefined; for (const t of this.store.values()) { @@ -43,7 +83,7 @@ export class MemoryDatabase implements Database { this.store.set(task.taskId, { ...task, - lastHeartbeat: DateTime.local().toString(), + lastHeartbeat: new Date().toISOString(), }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts index a5d90898f5..2491c9a091 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts @@ -20,6 +20,7 @@ import { TaskSpec, TaskBroker, DispatchResult, + DbTaskEventRow, } from './types'; import { MemoryDatabase } from './MemoryDatabase'; @@ -43,14 +44,22 @@ export class TaskAgent implements Task { } async emitLog(message: string): Promise { - throw new Error('Method not implemented.'); + await this.storage.emit({ + taskId: this.state.taskId, + runId: this.state.runId, + event: message, + }); } async complete(result: CompletedTaskState): Promise { - this.storage.setStatus( + await this.storage.setStatus( this.state.taskId, result === 'FAILED' ? 'FAILED' : 'COMPLETED', ); + + if (this.heartbeartInterval) { + clearInterval(this.heartbeartInterval); + } } private start() { @@ -58,7 +67,7 @@ export class TaskAgent implements Task { if (!this.state.runId) { throw new Error('no run id provided'); } - this.storage.heartBeat(this.state.runId); + this.storage.heartbeat(this.state.runId); }, 1000); } } @@ -66,7 +75,7 @@ export class TaskAgent implements Task { interface TaskState { spec: TaskSpec; taskId: string; - runId: string | undefined; + runId: string; } function defer() { @@ -87,7 +96,7 @@ export class MemoryTaskBroker implements TaskBroker { if (pendingTask) { return TaskAgent.create( { - runId: pendingTask.runId, + runId: pendingTask.runId!, taskId: pendingTask.taskId, spec: pendingTask.spec, }, @@ -107,6 +116,41 @@ export class MemoryTaskBroker implements TaskBroker { }; } + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: (result: { events: DbTaskEventRow[] }) => void, + ): () => void { + const { taskId } = options; + + let cancelled = false; + const unsubscribe = () => { + cancelled = true; + }; + + (async () => { + let after = options.after; + while (!cancelled) { + const result = await this.storage.getEvents({ taskId, after: after }); + const { events } = result; + if (events.length) { + after = events[events.length - 1].id; + try { + callback(result); + } catch (error) { + console.log('DEBUG: error =', error); + } + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + })(); + + return unsubscribe; + } + private waitForDispatch() { return this.deferredDispatch.promise; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts new file mode 100644 index 0000000000..dc15b3db65 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { TaskBroker, Task } from './types'; +import { Logger } from 'winston'; +import Docker from 'dockerode'; +import { CatalogEntityClient } from '../../lib/catalog'; +import { + FilePreparer, + parseLocationAnnotation, + PreparerBuilder, + TemplaterBuilder, + PublisherBuilder, +} from '../stages'; + +type Options = { + logger: Logger; + taskBroker: TaskBroker; + workingDirectory: string; + dockerClient: Docker; + entityClient: CatalogEntityClient; + preparers: PreparerBuilder; + templaters: TemplaterBuilder; + publishers: PublisherBuilder; +}; + +export class TaskWorker { + constructor(private readonly options: Options) {} + + start() { + (async () => { + for (;;) { + const task = await this.options.taskBroker.claim(); + await this.runOneTask(task); + } + })(); + } + + async runOneTask(task: Task) { + const { + dockerClient, + preparers, + templaters, + publishers, + workingDirectory, + logger, + } = this.options; + + try { + task.emitLog('Task claimed, waiting ...'); + // Give us some time to curl observe + await new Promise(resolve => setTimeout(resolve, 5000)); + + const { values, template } = task.spec; + task.emitLog('Prepare the skeleton'); + const { protocol, location: pullPath } = parseLocationAnnotation( + task.spec.template, + ); + + const preparer = + protocol === 'file' ? new FilePreparer() : preparers.get(pullPath); + const templater = templaters.get(template); + const publisher = publishers.get(values.storePath); + + const skeletonDir = await preparer.prepare(task.spec.template, { + logger, + workingDirectory: workingDirectory, + }); + + task.emitLog('Run the templater'); + const { resultDir } = await templater.run({ + directory: skeletonDir, + dockerClient, + logStream: process.stdout, // yay + values: values, + }); + + task.emitLog('Publish template'); + logger.info('Will now store the template'); + + logger.info('Totally storing the template now'); + await new Promise(resolve => setTimeout(resolve, 5000)); + // const result = await publisher.publish({ + // values: values, + // directory: resultDir, + // logger, + // }); + // task.emitLog(`Result: ${JSON.stringify(result)}`); + + task.emitLog(`Completely done now!`); + + await task.complete('COMPLETED'); + } catch (error) { + await task.complete('FAILED'); + } + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts new file mode 100644 index 0000000000..1cc9842fed --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { MemoryDatabase } from './MemoryDatabase'; +export { MemoryTaskBroker } from './MemoryTaskBroker'; +export { TaskWorker } from './TaskWorker'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index e8cea06005..12cb9fee1f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplaterValues } from '..'; + export type Status = | 'OPEN' | 'PROCESSING' @@ -35,13 +38,15 @@ export type DbTaskRow = { export type DbTaskEventRow = { id: number; - runId?: number; - stageName: string; + runId: string; + taskId: string; + event: string; createdAt: string; }; export type TaskSpec = { - metadata: string; + template: TemplateEntityV1alpha1; + values: TemplaterValues; }; export type DispatchResult = { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9098e5d05e..706027f62e 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -33,6 +33,11 @@ import { import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; import parseGitUrl from 'git-url-parse'; +import { + MemoryTaskBroker, + MemoryDatabase, + TaskWorker, +} from '../scaffolder/tasks'; export interface RouterOptions { preparers: PreparerBuilder; @@ -63,6 +68,18 @@ export async function createRouter( const logger = parentLogger.child({ plugin: 'scaffolder' }); const jobProcessor = await JobProcessor.fromConfig({ config, logger }); + const taskBroker = new MemoryTaskBroker(new MemoryDatabase()); + const worker = new TaskWorker({ + logger, + taskBroker, + workingDirectory: 'todo', + dockerClient, + entityClient, + preparers, + publishers, + templaters, + }); + worker.start(); router .get('/v1/job/:jobId', ({ params }, res) => { @@ -88,6 +105,62 @@ export async function createRouter( error: job.error, }); }) + // curl -X POST -d '{"templateName":"springboot-template","values": {"storePath":"https://github.com/jhaals/foo", "component_id":"woop", "description": "apa", "owner": "me" }}' -H 'Content-Type: application/json' localhost:7000/api/scaffolder/v2/tasks + .post('/v2/tasks', async (req, res) => { + const templateName: string = req.body.templateName; + const values: TemplaterValues = { + ...req.body.values, + destination: { + git: parseGitUrl(req.body.values.storePath), + }, + }; + const template = await entityClient.findTemplate(templateName); + + const validationResult: ValidatorResult = validate( + values, + template.spec.schema, + ); + + if (!validationResult.valid) { + res.status(400).json({ errors: validationResult.errors }); + return; + } + const result = await taskBroker.dispatch({ + template, + values, + }); + + res.status(201).json({ id: result.taskId }); + }) + + .get('/v2/tasks/:taskId/eventstream', async (req, res) => { + const { taskId } = req.params; + const after = Number(req.query.after) || undefined; + logger.info('event stream opened'); + + // Mandatory headers and http status to keep connection open + res.writeHead(200, { + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/event-stream', + }); + // After client opens connection send all nests as string + const unsubscribe = taskBroker.observe( + { taskId, after }, + ({ events }) => { + for (const event of events) { + res.write(`event:${JSON.stringify(event)}\n\n`); + } + }, + ); + // When client closes connection we update the clients list + // avoiding the disconnected one + req.on('close', () => { + unsubscribe(); + logger.info('event stream closed'); + }); + }) + .post('/v1/jobs', async (req, res) => { const templateName: string = req.body.templateName; const values: TemplaterValues = { From dfea89fa98562ef30713eba9db5942d8351f3e70 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 25 Jan 2021 10:12:15 +0100 Subject: [PATCH 022/158] Pass taskSpec in tests --- .../scaffolder/tasks/MemoryTaskBroker.test.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts index f1493ab3d0..aa23044749 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplaterValues } from '../stages/templater/types'; import { MemoryDatabase } from './MemoryDatabase'; import { MemoryTaskBroker, TaskAgent } from './MemoryTaskBroker'; @@ -21,10 +23,13 @@ describe('MemoryTaskBroker', () => { const storage = new MemoryDatabase(); const broker = new MemoryTaskBroker(storage); + const taskSpec = { + values: {} as TemplaterValues, + template: {} as TemplateEntityV1alpha1, + }; + it('should claim a dispatched work item', async () => { - await broker.dispatch({ - metadata: '', - }); + await broker.dispatch(taskSpec); await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); }); @@ -33,14 +38,23 @@ describe('MemoryTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); - await broker.dispatch({ metadata: 'foo' }); + await broker.dispatch(taskSpec); await expect(promise).resolves.toEqual(expect.any(TaskAgent)); }); it('should dispatch multiple items and claim them in order', async () => { - await broker.dispatch({ metadata: 'a' }); - await broker.dispatch({ metadata: 'b' }); - await broker.dispatch({ metadata: 'c' }); + await broker.dispatch({ + values: { owner: 'a' } as TemplaterValues, + template: {} as TemplateEntityV1alpha1, + }); + await broker.dispatch({ + values: { owner: 'b' } as TemplaterValues, + template: {} as TemplateEntityV1alpha1, + }); + await broker.dispatch({ + values: { owner: 'c' } as TemplaterValues, + template: {} as TemplateEntityV1alpha1, + }); const taskA = await broker.claim(); const taskB = await broker.claim(); @@ -48,13 +62,13 @@ describe('MemoryTaskBroker', () => { await expect(taskA).toEqual(expect.any(TaskAgent)); await expect(taskB).toEqual(expect.any(TaskAgent)); await expect(taskC).toEqual(expect.any(TaskAgent)); - await expect(taskA.spec.metadata).toBe('a'); - await expect(taskB.spec.metadata).toBe('b'); - await expect(taskC.spec.metadata).toBe('c'); + await expect(taskA.spec.values.owner).toBe('a'); + await expect(taskB.spec.values.owner).toBe('b'); + await expect(taskC.spec.values.owner).toBe('c'); }); it('should complete a task', async () => { - const dispatchResult = await broker.dispatch({ metadata: 'foo' }); + const dispatchResult = await broker.dispatch(taskSpec); const task = await broker.claim(); await task.complete('COMPLETED'); const taskRow = await storage.get(dispatchResult.taskId); @@ -62,7 +76,7 @@ describe('MemoryTaskBroker', () => { }); it('should fail a task', async () => { - const dispatchResult = await broker.dispatch({ metadata: 'foo' }); + const dispatchResult = await broker.dispatch(taskSpec); const task = await broker.claim(); await task.complete('FAILED'); const taskRow = await storage.get(dispatchResult.taskId); From 7d13f22c3db614d9be40b6652e42fdb07bb5185f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 25 Jan 2021 13:16:28 +0100 Subject: [PATCH 023/158] Add eventType for closing observer stream Co-authored-by: Patrik Oldsberg --- .../src/scaffolder/tasks/MemoryDatabase.ts | 22 +++++++++++++------ .../scaffolder/tasks/MemoryTaskBroker.test.ts | 8 +++---- .../src/scaffolder/tasks/MemoryTaskBroker.ts | 12 +++++++--- .../src/scaffolder/tasks/TaskWorker.ts | 4 ++-- .../src/scaffolder/tasks/types.ts | 16 ++++++++------ .../scaffolder-backend/src/service/router.ts | 4 ++++ 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts index 69fb6a1567..b2d1cf667b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { DbTaskRow, DbTaskEventRow, Status, TaskSpec } from './types'; +import { + DbTaskRow, + DbTaskEventRow, + Status, + TaskSpec, + TaskEventType, +} from './types'; import { v4 as uuid } from 'uuid'; export interface Database { @@ -28,7 +34,8 @@ export interface Database { type EmitOptions = { taskId: string; runId: string; - event: string; + body: string; + type: TaskEventType; }; type ReadOptions = { @@ -40,12 +47,13 @@ export class MemoryDatabase implements Database { private readonly store = new Map(); private readonly events = new Array(); - async emit({ taskId, runId, event }: EmitOptions) { + async emit({ taskId, runId, body, type }: EmitOptions) { this.events.push({ id: this.events.length, taskId, runId, - event, + body, + type, createdAt: new Date().toISOString(), }); } @@ -89,10 +97,10 @@ export class MemoryDatabase implements Database { async claimTask(): Promise { for (const t of this.store.values()) { - if (t.status === 'OPEN') { + if (t.status === 'open') { const task: DbTaskRow = { ...t, - status: 'PROCESSING', + status: 'processing', runId: uuid(), }; this.store.set(t.taskId, task); @@ -106,7 +114,7 @@ export class MemoryDatabase implements Database { const taskRow = { taskId: uuid(), spec, - status: 'OPEN' as Status, + status: 'open' as Status, retryCount: 0, createdAt: new Date().toISOString(), }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts index aa23044749..1ac416ad4f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts @@ -70,16 +70,16 @@ describe('MemoryTaskBroker', () => { it('should complete a task', async () => { const dispatchResult = await broker.dispatch(taskSpec); const task = await broker.claim(); - await task.complete('COMPLETED'); + await task.complete('completed'); const taskRow = await storage.get(dispatchResult.taskId); - expect(taskRow.status).toBe('COMPLETED'); + expect(taskRow.status).toBe('completed'); }); it('should fail a task', async () => { const dispatchResult = await broker.dispatch(taskSpec); const task = await broker.claim(); - await task.complete('FAILED'); + await task.complete('failed'); const taskRow = await storage.get(dispatchResult.taskId); - expect(taskRow.status).toBe('FAILED'); + expect(taskRow.status).toBe('failed'); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts index 2491c9a091..c22bb0bddb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts @@ -47,16 +47,22 @@ export class TaskAgent implements Task { await this.storage.emit({ taskId: this.state.taskId, runId: this.state.runId, - event: message, + body: message, + type: 'log', }); } async complete(result: CompletedTaskState): Promise { await this.storage.setStatus( this.state.taskId, - result === 'FAILED' ? 'FAILED' : 'COMPLETED', + result === 'failed' ? 'failed' : 'completed', ); - + this.storage.emit({ + taskId: this.state.taskId, + runId: this.state.runId, + body: `Run completed with status: ${result}`, + type: 'completion', + }); if (this.heartbeartInterval) { clearInterval(this.heartbeartInterval); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index dc15b3db65..641a8cd9b1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -102,9 +102,9 @@ export class TaskWorker { task.emitLog(`Completely done now!`); - await task.complete('COMPLETED'); + await task.complete('completed'); } catch (error) { - await task.complete('FAILED'); + await task.complete('failed'); } } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 12cb9fee1f..47226dc6e9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -18,13 +18,13 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { TemplaterValues } from '..'; export type Status = - | 'OPEN' - | 'PROCESSING' - | 'FAILED' - | 'CANCELLED' - | 'COMPLETED'; + | 'open' + | 'processing' + | 'failed' + | 'cancelled' + | 'completed'; -export type CompletedTaskState = 'FAILED' | 'COMPLETED'; +export type CompletedTaskState = 'failed' | 'completed'; export type DbTaskRow = { taskId: string; @@ -36,11 +36,13 @@ export type DbTaskRow = { runId?: string; }; +export type TaskEventType = 'completion' | 'log'; export type DbTaskEventRow = { id: number; runId: string; taskId: string; - event: string; + body: string; + type: TaskEventType; createdAt: string; }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 706027f62e..9514d215d0 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -150,6 +150,10 @@ export async function createRouter( ({ events }) => { for (const event of events) { res.write(`event:${JSON.stringify(event)}\n\n`); + if (event.type === 'completion') { + unsubscribe(); + res.end(); + } } }, ); From fef53431df1c3745bfb210eb6b8cf8a637b75213 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 25 Jan 2021 14:02:51 +0100 Subject: [PATCH 024/158] create logger and stream that emit events --- .../src/scaffolder/tasks/TaskWorker.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 641a8cd9b1..d500fc8e5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -14,9 +14,11 @@ * limitations under the License. */ -import { TaskBroker, Task } from './types'; +import { PassThrough } from 'stream'; import { Logger } from 'winston'; +import * as winston from 'winston'; import Docker from 'dockerode'; +import { TaskBroker, Task } from './types'; import { CatalogEntityClient } from '../../lib/catalog'; import { FilePreparer, @@ -59,6 +61,24 @@ export class TaskWorker { logger, } = this.options; + const taskLogger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp(), + winston.format.simple(), + ), + defaultMeta: {}, + }); + + const stream = new PassThrough(); + stream.on('data', data => { + const message = data.toString().trim(); + if (message?.length > 1) task.emitLog(message); + }); + + taskLogger.add(new winston.transports.Stream({ stream })); + try { task.emitLog('Task claimed, waiting ...'); // Give us some time to curl observe @@ -76,7 +96,7 @@ export class TaskWorker { const publisher = publishers.get(values.storePath); const skeletonDir = await preparer.prepare(task.spec.template, { - logger, + logger: taskLogger, workingDirectory: workingDirectory, }); @@ -84,7 +104,7 @@ export class TaskWorker { const { resultDir } = await templater.run({ directory: skeletonDir, dockerClient, - logStream: process.stdout, // yay + logStream: stream, values: values, }); @@ -100,8 +120,6 @@ export class TaskWorker { // }); // task.emitLog(`Result: ${JSON.stringify(result)}`); - task.emitLog(`Completely done now!`); - await task.complete('completed'); } catch (error) { await task.complete('failed'); From 8623025c9b789b85bdf87b9cdf9cc94995f55859 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 26 Jan 2021 12:00:40 +0100 Subject: [PATCH 025/158] Wip wip --- .../src/scaffolder/stages/legacy.ts | 106 +++++++++++++++++ .../scaffolder/tasks/MemoryTaskBroker.test.ts | 2 +- .../src/scaffolder/tasks/TemplateConverter.ts | 110 ++++++++++++++++++ .../src/scaffolder/tasks/types.ts | 11 +- .../scaffolder-backend/src/service/router.ts | 2 + 5 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts new file mode 100644 index 0000000000..c9bd89c3dd --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Config } from '@backstage/config'; +import { TemplateActionRegistry } from '../TemplateConverter'; +import { FilePreparer } from './prepare'; +import Docker from 'dockerode'; + +type Options = { + logger: Logger; + config: Config; + dockerClient: Docker; +}; + +export function registerLegacyActions( + registry: TemplateActionRegistry, + options: Options, +) { + registry.register({ + id: 'legacy:prepare', + async handler(ctx) { + const { logger } = ctx; + console.log(ctx); + logger.info('Task claimed, waiting ...'); + // Give us some time to curl observe + await new Promise(resolve => setTimeout(resolve, 5000)); + + logger.info('Prepare the skeleton'); + + const { protocol, pullPath } = ctx.parameters; + + const preparer = + protocol === 'file' + ? new FilePreparer() + : preparers.get(pullPath as string); + + await preparer.prepare(task.spec.template, { + logger, + ctx.workspaceDir, + }); + ctx.output('catalogInfoUrl', 'httpderp://asdasd'); + }, + }); + + // try { + // const { values, template } = task.spec; + // task.emitLog('Prepare the skeleton'); + // const { protocol, location: pullPath } = parseLocationAnnotation( + // task.spec.template, + // ); + // const preparer = + // protocol === 'file' ? new FilePreparer() : preparers.get(pullPath); + // const templater = templaters.get(template); + // const publisher = publishers.get(values.storePath); + + // const skeletonDir = await preparer.prepare(task.spec.template, { + // logger: taskLogger, + // workingDirectory: workingDirectory, + // }); + + registry.register({ + id: 'legacy:template', + async handler(ctx) { + const { logger } = ctx; + + const templater = templaters.get(ctx.parameters.templater as string); + + logger.info('Run the templater'); + const { resultDir } = await templater.run({ + directory: ctx.workspaceDir, + dockerClient, + logStream: ctx.logStream, + values: ctx.parameters.values as TemplaterValues, + }); + }, + }); + // task.emitLog('Publish template'); + // logger.info('Will now store the template'); + + // logger.info('Totally storing the template now'); + // await new Promise(resolve => setTimeout(resolve, 5000)); + // // const result = await publisher.publish({ + // // values: values, + // // directory: resultDir, + // // logger, + // // }); + // // task.emitLog(`Result: ${JSON.stringify(result)}`); + + // await task.complete('completed'); + // } catch (error) { + // await task.complete('failed'); + // } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts index 1ac416ad4f..686a6d9132 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts @@ -15,7 +15,7 @@ */ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { TemplaterValues } from '../stages/templater/types'; +import { TemplaterValues } from './actions/templater/types'; import { MemoryDatabase } from './MemoryDatabase'; import { MemoryTaskBroker, TaskAgent } from './MemoryTaskBroker'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts new file mode 100644 index 0000000000..e435812fe5 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { JsonValue } from '@backstage/config'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { Logger } from 'winston'; +import type { Writable } from 'stream'; +import { + getTemplaterKey, + parseLocationAnnotation, + TemplaterValues, +} from '../jobs/actions'; +import { TaskSpec } from './types'; +import { ConflictError, NotFoundError } from '@backstage/backend-common'; + +function templateEntityToSpec( + template: TemplateEntityV1alpha1, + values: TemplaterValues, +): TaskSpec { + const steps: TaskSpec['steps'] = []; + + const { protocol, location: pullPath } = parseLocationAnnotation(template); + const templater = getTemplaterKey(template); + + steps.push({ + id: 'prepare', + name: 'Prepare', + action: 'legacy:prepare', + parameters: { + protocol, + pullPath, + }, + }); + + steps.push({ + id: 'template', + name: 'Template', + action: 'legacy:template', + parameters: { + templater, + values, + }, + }); + + steps.push({ + id: 'publish', + name: 'Publishing', + action: 'publish', + parameters: { + values, + directory, + }, + }); + + return { steps }; +} + +type ActionContext = { + logger: Logger; + logStream: Writable; + + workspaceDir: string; + parameters: { [name: string]: JsonValue }; + output(name: string, value: JsonValue): void; +}; + +type TemplateAction = { + id: string; + handler: (ctx: ActionContext) => Promise; +}; + +export class TemplateActionRegistry { + private readonly actions = new Map(); + + register(action: TemplateAction) { + if (this.actions.has(action.id)) { + throw new ConflictError( + `Template action with id ${action.id} as already been registered`, + ); + } + this.actions.set(action.id, action); + } + + // validate + // ensure that action exist. + // template variables exist. + + get(actionId: string): TemplateAction { + const action = this.actions.get(actionId); + if (!action) { + throw new NotFoundError( + `Template action with id ${actionId} is not registered.`, + ); + } + return action; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 47226dc6e9..620b6e7243 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { TemplaterValues } from '..'; +import { JsonObject } from '@backstage/config'; export type Status = | 'open' @@ -47,8 +46,12 @@ export type DbTaskEventRow = { }; export type TaskSpec = { - template: TemplateEntityV1alpha1; - values: TemplaterValues; + steps: Array<{ + id: string; + name: string; + action: string; + parameters?: JsonObject; + }>; }; export type DispatchResult = { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9514d215d0..fc40d45ce2 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -38,6 +38,8 @@ import { MemoryDatabase, TaskWorker, } from '../scaffolder/tasks'; +import { TemplateActionRegistry } from '../scaffolder/tasks/TemplateConverter'; +import { LOCATION_ANNOTATION } from '@backstage/catalog-model'; export interface RouterOptions { preparers: PreparerBuilder; From cf896972ef873b4a94f669d10b9a8f5a3e186e00 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 13:16:50 +0100 Subject: [PATCH 026/158] Wire up task registry with legacy tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .../src/scaffolder/stages/legacy.ts | 105 ++++++------ .../src/scaffolder/tasks/MemoryTaskBroker.ts | 8 + .../src/scaffolder/tasks/TaskWorker.ts | 155 ++++++++++-------- .../src/scaffolder/tasks/TemplateConverter.ts | 31 ++-- .../src/scaffolder/tasks/types.ts | 6 +- .../scaffolder-backend/src/service/helpers.ts | 44 +++++ .../scaffolder-backend/src/service/router.ts | 23 ++- 7 files changed, 242 insertions(+), 130 deletions(-) create mode 100644 plugins/scaffolder-backend/src/service/helpers.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts index c9bd89c3dd..6244e40d68 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts @@ -14,21 +14,25 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import { TemplateActionRegistry } from '../TemplateConverter'; -import { FilePreparer } from './prepare'; +import { TemplateActionRegistry } from '../tasks/TemplateConverter'; +import { FilePreparer, PreparerBuilder } from './prepare'; import Docker from 'dockerode'; +import { TemplaterBuilder, TemplaterValues } from './templater'; +import { PublisherBuilder } from './publish'; type Options = { - logger: Logger; - config: Config; dockerClient: Docker; + preparers: PreparerBuilder; + templaters: TemplaterBuilder; + publishers: PublisherBuilder; }; export function registerLegacyActions( registry: TemplateActionRegistry, options: Options, ) { + const { dockerClient, preparers, templaters, publishers } = options; + registry.register({ id: 'legacy:prepare', async handler(ctx) { @@ -41,36 +45,18 @@ export function registerLegacyActions( logger.info('Prepare the skeleton'); const { protocol, pullPath } = ctx.parameters; - + const url = pullPath as string; const preparer = - protocol === 'file' - ? new FilePreparer() - : preparers.get(pullPath as string); + protocol === 'file' ? new FilePreparer() : preparers.get(url); - await preparer.prepare(task.spec.template, { - logger, - ctx.workspaceDir, + await preparer.prepare({ + url, + logger: ctx.logger, + workspacePath: ctx.workspacePath, }); - ctx.output('catalogInfoUrl', 'httpderp://asdasd'); }, }); - // try { - // const { values, template } = task.spec; - // task.emitLog('Prepare the skeleton'); - // const { protocol, location: pullPath } = parseLocationAnnotation( - // task.spec.template, - // ); - // const preparer = - // protocol === 'file' ? new FilePreparer() : preparers.get(pullPath); - // const templater = templaters.get(template); - // const publisher = publishers.get(values.storePath); - - // const skeletonDir = await preparer.prepare(task.spec.template, { - // logger: taskLogger, - // workingDirectory: workingDirectory, - // }); - registry.register({ id: 'legacy:template', async handler(ctx) { @@ -79,28 +65,55 @@ export function registerLegacyActions( const templater = templaters.get(ctx.parameters.templater as string); logger.info('Run the templater'); - const { resultDir } = await templater.run({ - directory: ctx.workspaceDir, + await templater.run({ + workspacePath: ctx.workspacePath, dockerClient, logStream: ctx.logStream, values: ctx.parameters.values as TemplaterValues, }); }, }); - // task.emitLog('Publish template'); - // logger.info('Will now store the template'); - // logger.info('Totally storing the template now'); - // await new Promise(resolve => setTimeout(resolve, 5000)); - // // const result = await publisher.publish({ - // // values: values, - // // directory: resultDir, - // // logger, - // // }); - // // task.emitLog(`Result: ${JSON.stringify(result)}`); - - // await task.complete('completed'); - // } catch (error) { - // await task.complete('failed'); - // } + registry.register({ + id: 'legacy:publish', + async handler(ctx) { + const { values } = ctx.parameters; + if ( + typeof values !== 'object' || + values === null || + Array.isArray(values) + ) { + throw new Error( + `Invalid values passed to publish, got ${typeof values}`, + ); + } + const storePath = values.storePath as unknown; + if (typeof storePath !== 'string') { + throw new Error( + `Invalid store path passed to publish, got ${typeof storePath}`, + ); + } + const owner = values.owner as unknown; + if (typeof owner !== 'string') { + throw new Error( + `Invalid store path passed to publish, got ${typeof owner}`, + ); + } + const publisher = publishers.get(storePath); + ctx.logger.info('Will now store the template'); + const { remoteUrl, catalogInfoUrl } = await publisher.publish({ + values: { + ...values, + owner, + storePath, + }, + workspacePath: ctx.workspacePath, + logger: ctx.logger, + }); + ctx.output('remoteUrl', remoteUrl); + if (catalogInfoUrl) { + ctx.output('catalogInfoUrl', catalogInfoUrl); + } + }, + }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts index c22bb0bddb..c5920fe836 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts @@ -43,6 +43,14 @@ export class TaskAgent implements Task { return this.state.spec; } + get runId() { + return this.state.runId; + } + + get taskId() { + return this.state.taskId; + } + async emitLog(message: string): Promise { await this.storage.emit({ taskId: this.state.taskId, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index d500fc8e5c..6eb1c4763c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -17,26 +17,17 @@ import { PassThrough } from 'stream'; import { Logger } from 'winston'; import * as winston from 'winston'; -import Docker from 'dockerode'; +import { JsonValue } from '@backstage/config'; import { TaskBroker, Task } from './types'; -import { CatalogEntityClient } from '../../lib/catalog'; -import { - FilePreparer, - parseLocationAnnotation, - PreparerBuilder, - TemplaterBuilder, - PublisherBuilder, -} from '../stages'; +import { TemplateActionRegistry } from './TemplateConverter'; +import fs from 'fs-extra'; +import path from 'path'; type Options = { logger: Logger; taskBroker: TaskBroker; workingDirectory: string; - dockerClient: Docker; - entityClient: CatalogEntityClient; - preparers: PreparerBuilder; - templaters: TemplaterBuilder; - publishers: PublisherBuilder; + actionRegistry: TemplateActionRegistry; }; export class TaskWorker { @@ -52,66 +43,90 @@ export class TaskWorker { } async runOneTask(task: Task) { - const { - dockerClient, - preparers, - templaters, - publishers, - workingDirectory, - logger, - } = this.options; - - const taskLogger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.colorize(), - winston.format.timestamp(), - winston.format.simple(), - ), - defaultMeta: {}, - }); - - const stream = new PassThrough(); - stream.on('data', data => { - const message = data.toString().trim(); - if (message?.length > 1) task.emitLog(message); - }); - - taskLogger.add(new winston.transports.Stream({ stream })); - try { - task.emitLog('Task claimed, waiting ...'); + const { actionRegistry, logger } = this.options; + + // bbl LUUUNCH + // taskID and runId not part of task? O_o + const workspacePath = await this.createWorkPath(task.taskId, task.runId); + + const taskLogger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp(), + winston.format.simple(), + ), + defaultMeta: {}, + }); + + const stream = new PassThrough(); + stream.on('data', data => { + const message = data.toString().trim(); + if (message?.length > 1) task.emitLog(message); + }); + + taskLogger.add(new winston.transports.Stream({ stream })); + // Give us some time to curl observe + task.emitLog('Task claimed, waiting ...'); await new Promise(resolve => setTimeout(resolve, 5000)); + task.emitLog(`Starting up work with ${task.spec.steps.length} steps`); - const { values, template } = task.spec; - task.emitLog('Prepare the skeleton'); - const { protocol, location: pullPath } = parseLocationAnnotation( - task.spec.template, - ); + const outputs: { [name: string]: JsonValue } = {}; - const preparer = - protocol === 'file' ? new FilePreparer() : preparers.get(pullPath); - const templater = templaters.get(template); - const publisher = publishers.get(values.storePath); + for (const step of task.spec.steps) { + task.emitLog(`Beginning step ${step.name}`); - const skeletonDir = await preparer.prepare(task.spec.template, { - logger: taskLogger, - workingDirectory: workingDirectory, - }); + const action = actionRegistry.get(step.action); + if (!action) { + throw new Error(`Action '${step.action}' does not exist`); + } - task.emitLog('Run the templater'); - const { resultDir } = await templater.run({ - directory: skeletonDir, - dockerClient, - logStream: stream, - values: values, - }); + // TODO: substitute any placeholders with output from previous steps + const parameters = step.parameters!; - task.emitLog('Publish template'); - logger.info('Will now store the template'); + await action.handler({ + logger, + logStream: stream, + parameters, + workspacePath, + output(name: string, value: JsonValue) { + outputs[name] = value; + }, + }); - logger.info('Totally storing the template now'); + task.emitLog(`Finished step ${step.name}`); + } + + // const { values, template } = task.spec; + // task.emitLog('Prepare the skeleton'); + // const { protocol, location: pullPath } = parseLocationAnnotation( + // task.spec.template, + // ); + + // const preparer = + // protocol === 'file' ? new FilePreparer() : preparers.get(pullPath); + // const templater = templaters.get(template); + // const publisher = publishers.get(values.storePath); + + // const skeletonDir = await preparer.prepare(task.spec.template, { + // logger: taskLogger, + // workingDirectory: workingDirectory, + // }); + + // task.emitLog('Run the templater'); + // const { resultDir } = await templater.run({ + // directory: skeletonDir, + // dockerClient, + // logStream: stream, + // values: values, + // }); + + // task.emitLog('Publish template'); + // logger.info('Will now store the template'); + + logger.info('So done right now'); await new Promise(resolve => setTimeout(resolve, 5000)); // const result = await publisher.publish({ // values: values, @@ -125,4 +140,14 @@ export class TaskWorker { await task.complete('failed'); } } + + async createWorkPath(taskId: string, runId: string): Promise { + const workspacePath = path.join( + this.options.workingDirectory, + taskId, + runId, + ); + fs.ensureDir(workspacePath); + return workspacePath; + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts index e435812fe5..bd06dbaa91 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts @@ -14,25 +14,37 @@ * limitations under the License. */ +import { resolve as resolvePath } from 'path'; import { JsonValue } from '@backstage/config'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Logger } from 'winston'; import type { Writable } from 'stream'; -import { - getTemplaterKey, - parseLocationAnnotation, - TemplaterValues, -} from '../jobs/actions'; + import { TaskSpec } from './types'; import { ConflictError, NotFoundError } from '@backstage/backend-common'; +import { + getTemplaterKey, + joinGitUrlPath, + parseLocationAnnotation, + TemplaterValues, +} from '../stages'; -function templateEntityToSpec( +export function templateEntityToSpec( template: TemplateEntityV1alpha1, values: TemplaterValues, ): TaskSpec { const steps: TaskSpec['steps'] = []; - const { protocol, location: pullPath } = parseLocationAnnotation(template); + const { protocol, location } = parseLocationAnnotation(template); + + let url: string; + if (protocol === 'file') { + const path = resolvePath(location, template.spec.path || '.'); + + url = `file://${path}`; + } else { + url = joinGitUrlPath(location, template.spec.path); + } const templater = getTemplaterKey(template); steps.push({ @@ -41,7 +53,7 @@ function templateEntityToSpec( action: 'legacy:prepare', parameters: { protocol, - pullPath, + url, }, }); @@ -61,7 +73,6 @@ function templateEntityToSpec( action: 'publish', parameters: { values, - directory, }, }); @@ -72,7 +83,7 @@ type ActionContext = { logger: Logger; logStream: Writable; - workspaceDir: string; + workspacePath: string; parameters: { [name: string]: JsonValue }; output(name: string, value: JsonValue): void; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 620b6e7243..496385d4ea 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonValue } from '@backstage/config'; export type Status = | 'open' @@ -50,7 +50,7 @@ export type TaskSpec = { id: string; name: string; action: string; - parameters?: JsonObject; + parameters?: { [name: string]: JsonValue }; }>; }; @@ -60,6 +60,8 @@ export type DispatchResult = { export interface Task { spec: TaskSpec; + taskId: string; + runId: string; emitLog(message: string): Promise; complete(result: CompletedTaskState): Promise; } diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts new file mode 100644 index 0000000000..dd3d43c6d1 --- /dev/null +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 os from 'os'; +import fs from 'fs-extra'; +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; + +export async function getWorkingDirectory( + config: Config, + logger: Logger, +): Promise { + if (!config.has('backend.workingDirectory')) { + return os.tmpdir(); + } + + const workingDirectory = config.getString('backend.workingDirectory'); + try { + // Check if working directory exists and is writable + await fs.access(workingDirectory, fs.constants.F_OK | fs.constants.W_OK); + logger.info(`using working directory: ${workingDirectory}`); + } catch (err) { + logger.error( + `working directory ${workingDirectory} ${ + err.code === 'ENOENT' ? 'does not exist' : 'is not writable' + }`, + ); + throw err; + } + return workingDirectory; +} diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index fc40d45ce2..a72a5c5d48 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -38,8 +38,13 @@ import { MemoryDatabase, TaskWorker, } from '../scaffolder/tasks'; -import { TemplateActionRegistry } from '../scaffolder/tasks/TemplateConverter'; +import { + TemplateActionRegistry, + templateEntityToSpec, +} from '../scaffolder/tasks/TemplateConverter'; import { LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { registerLegacyActions } from '../scaffolder/stages/legacy'; +import { getWorkingDirectory } from './helpers'; export interface RouterOptions { preparers: PreparerBuilder; @@ -69,18 +74,24 @@ export async function createRouter( } = options; const logger = parentLogger.child({ plugin: 'scaffolder' }); + const workingDirectory = await getWorkingDirectory(config, logger); const jobProcessor = await JobProcessor.fromConfig({ config, logger }); const taskBroker = new MemoryTaskBroker(new MemoryDatabase()); + const actionRegistry = new TemplateActionRegistry(); const worker = new TaskWorker({ logger, taskBroker, - workingDirectory: 'todo', + actionRegistry, + workingDirectory, + }); + + registerLegacyActions(actionRegistry, { dockerClient, - entityClient, preparers, publishers, templaters, }); + worker.start(); router @@ -127,10 +138,8 @@ export async function createRouter( res.status(400).json({ errors: validationResult.errors }); return; } - const result = await taskBroker.dispatch({ - template, - values, - }); + const taskSpec = templateEntityToSpec(template, values); + const result = await taskBroker.dispatch(taskSpec); res.status(201).json({ id: result.taskId }); }) From abff060e67dbacacdf3343a20df927390fd8b802 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 15:56:29 +0100 Subject: [PATCH 027/158] Use url from parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .../scaffolder-backend/src/scaffolder/stages/legacy.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts index 6244e40d68..a3fe01c96d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts @@ -40,17 +40,15 @@ export function registerLegacyActions( console.log(ctx); logger.info('Task claimed, waiting ...'); // Give us some time to curl observe - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise(resolve => setTimeout(resolve, 1000)); logger.info('Prepare the skeleton'); - - const { protocol, pullPath } = ctx.parameters; - const url = pullPath as string; + const { protocol, url } = ctx.parameters; const preparer = - protocol === 'file' ? new FilePreparer() : preparers.get(url); + protocol === 'file' ? new FilePreparer() : preparers.get(url as string); await preparer.prepare({ - url, + url: url as string, logger: ctx.logger, workspacePath: ctx.workspacePath, }); From 7b7230f75bdd1d7a4c7513f11e9451b58f1f41f1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 15:57:37 +0100 Subject: [PATCH 028/158] Add getWorkspaceName --- .../src/scaffolder/tasks/MemoryTaskBroker.ts | 8 +-- .../src/scaffolder/tasks/TaskWorker.ts | 54 +++---------------- .../src/scaffolder/tasks/types.ts | 3 +- 3 files changed, 10 insertions(+), 55 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts index c5920fe836..6937c535dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts @@ -43,12 +43,8 @@ export class TaskAgent implements Task { return this.state.spec; } - get runId() { - return this.state.runId; - } - - get taskId() { - return this.state.taskId; + async getWorkspaceName() { + return `${this.state.taskId}_${this.state.runId}`; } async emitLog(message: string): Promise { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 6eb1c4763c..c80a1b81bf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -19,9 +19,9 @@ import { Logger } from 'winston'; import * as winston from 'winston'; import { JsonValue } from '@backstage/config'; import { TaskBroker, Task } from './types'; -import { TemplateActionRegistry } from './TemplateConverter'; import fs from 'fs-extra'; import path from 'path'; +import { TemplateActionRegistry } from './TemplateConverter'; type Options = { logger: Logger; @@ -46,9 +46,11 @@ export class TaskWorker { try { const { actionRegistry, logger } = this.options; - // bbl LUUUNCH - // taskID and runId not part of task? O_o - const workspacePath = await this.createWorkPath(task.taskId, task.runId); + const workspacePath = path.join( + this.options.workingDirectory, + await task.getWorkspaceName(), + ); + await fs.ensureDir(workspacePath); const taskLogger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', @@ -99,55 +101,13 @@ export class TaskWorker { task.emitLog(`Finished step ${step.name}`); } - // const { values, template } = task.spec; - // task.emitLog('Prepare the skeleton'); - // const { protocol, location: pullPath } = parseLocationAnnotation( - // task.spec.template, - // ); - - // const preparer = - // protocol === 'file' ? new FilePreparer() : preparers.get(pullPath); - // const templater = templaters.get(template); - // const publisher = publishers.get(values.storePath); - - // const skeletonDir = await preparer.prepare(task.spec.template, { - // logger: taskLogger, - // workingDirectory: workingDirectory, - // }); - - // task.emitLog('Run the templater'); - // const { resultDir } = await templater.run({ - // directory: skeletonDir, - // dockerClient, - // logStream: stream, - // values: values, - // }); - - // task.emitLog('Publish template'); - // logger.info('Will now store the template'); - logger.info('So done right now'); await new Promise(resolve => setTimeout(resolve, 5000)); - // const result = await publisher.publish({ - // values: values, - // directory: resultDir, - // logger, - // }); - // task.emitLog(`Result: ${JSON.stringify(result)}`); await task.complete('completed'); } catch (error) { + task.emitLog(error); await task.complete('failed'); } } - - async createWorkPath(taskId: string, runId: string): Promise { - const workspacePath = path.join( - this.options.workingDirectory, - taskId, - runId, - ); - fs.ensureDir(workspacePath); - return workspacePath; - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 496385d4ea..d3c08085fd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -60,10 +60,9 @@ export type DispatchResult = { export interface Task { spec: TaskSpec; - taskId: string; - runId: string; emitLog(message: string): Promise; complete(result: CompletedTaskState): Promise; + getWorkspaceName(): Promise; } export interface TaskBroker { From 95c2719809adec52fb1979757559e1172e1e3073 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 15:58:20 +0100 Subject: [PATCH 029/158] Rename publish to legacy:publish --- .../src/scaffolder/tasks/TemplateConverter.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts index bd06dbaa91..86f722e475 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts @@ -70,7 +70,7 @@ export function templateEntityToSpec( steps.push({ id: 'publish', name: 'Publishing', - action: 'publish', + action: 'legacy:publish', parameters: { values, }, @@ -105,10 +105,6 @@ export class TemplateActionRegistry { this.actions.set(action.id, action); } - // validate - // ensure that action exist. - // template variables exist. - get(actionId: string): TemplateAction { const action = this.actions.get(actionId); if (!action) { From 605eb7f59401429fa5ab48ec83f4acf0a313dbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 Jan 2021 15:36:36 +0100 Subject: [PATCH 030/158] scaffolder: add the bare minimum of a db abstraction --- plugins/scaffolder-backend/knexfile.js | 25 +++++ .../migrations/20210120143715_init.js | 100 ++++++++++++++++++ plugins/scaffolder-backend/package.json | 3 +- .../scaffolder-backend/src/tasks/Database.ts | 70 ++++++++++++ plugins/scaffolder-backend/src/tasks/types.ts | 37 +++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 plugins/scaffolder-backend/knexfile.js create mode 100644 plugins/scaffolder-backend/migrations/20210120143715_init.js create mode 100644 plugins/scaffolder-backend/src/tasks/Database.ts create mode 100644 plugins/scaffolder-backend/src/tasks/types.ts diff --git a/plugins/scaffolder-backend/knexfile.js b/plugins/scaffolder-backend/knexfile.js new file mode 100644 index 0000000000..f469df4c08 --- /dev/null +++ b/plugins/scaffolder-backend/knexfile.js @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +module.exports = { + development: { + client: 'sqlite3', + connection: ':memory:', + migrations: { + directory: 'migrations', + }, + }, +}; diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js new file mode 100644 index 0000000000..d2535bfa86 --- /dev/null +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -0,0 +1,100 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('tasks', table => { + table.comment('The table of scaffolder tasks'); + table.uuid('id').primary().notNullable().comment('The ID of the task'); + table + .text('context') + .notNullable() + .comment('A JSON object with task specific context information'); + table + .text('spec') + .notNullable() + .comment('A JSON encoded task specification'); + table + .text('status') + .notNullable() + .comment('The current status of the task'); + table + .integer('run_id') + .nullable() + .comment('The current run ID of the task'); + table + .dateTime('created_at') + .notNullable() + .comment('The timestamp when this task was created'); + table + .dateTime('last_heartbeat_at') + .nullable() + .comment('The last timestamp when a heartbeat was received'); + table + .integer('retry_count') + .notNullable() + .defaultTo(0) + .comment('The number of times that this task has been attempted'); + }); + await knex.schema.createTable('task_events', table => { + table.comment('The event stream a given task'); + table + .bigIncrements('id') + .primary() + .notNullable() + .comment('The ID of the event'); + table + .uuid('task_id') + .references('id') + .inTable('tasks') + .notNullable() + .onDelete('CASCADE') + .comment('The task that generated the event'); + table + .integer('run_id') + .nullable() + .comment('The run ID of the task that this event applies to'); + table + .text('stage_name') + .nullable() + .comment('The stage of the task that this event applies to'); + table.text('event_type').notNullable().comment('The type of event'); + table + .dateTime('created_at') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this event was generated'); + table.text('text').notNullable().comment('The text of the event'); + table.index(['task_id'], 'task_events_task_id_idx'); + }); +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + if (knex.client.config.client !== 'sqlite3') { + await knex.schema.alterTable('task_events', table => { + table.dropIndex([], 'task_events_task_id_idx'); + }); + } + await knex.schema.dropTable('task_events'); + await knex.schema.dropTable('tasks'); +}; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 97ab723e81..5d024c2798 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -53,7 +53,7 @@ "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", - "luxon": "^1.25.0", + "knex": "^0.21.6", "morgan": "^1.10.0", "p-queue": "^6.3.0", "uuid": "^8.2.0", @@ -73,6 +73,7 @@ }, "files": [ "dist", + "migrations", "config.d.ts" ], "configSchema": "config.d.ts" diff --git a/plugins/scaffolder-backend/src/tasks/Database.ts b/plugins/scaffolder-backend/src/tasks/Database.ts new file mode 100644 index 0000000000..c135380f87 --- /dev/null +++ b/plugins/scaffolder-backend/src/tasks/Database.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ConflictError, resolvePackagePath } from '@backstage/backend-common'; +import Knex from 'knex'; +import { Logger } from 'winston'; +import { Database, Transaction } from './types'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'migrations', +); + +export class CommonDatabase implements Database { + static async create(knex: Knex, logger: Logger): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new CommonDatabase(knex, logger); + } + + constructor( + private readonly database: Knex, + private readonly logger: Logger, + ) {} + + async transaction(fn: (tx: Transaction) => Promise): Promise { + try { + let result: T | undefined = undefined; + + await this.database.transaction( + async tx => { + // We can't return here, as knex swallows the return type in case the transaction is rolled back: + // https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/transaction.js#L136 + result = await fn(tx); + }, + { + // If we explicitly trigger a rollback, don't fail. + doNotRejectOnRollback: true, + }, + ); + + return result!; + } catch (e) { + this.logger.debug(`Error during transaction, ${e}`); + + if ( + /SQLITE_CONSTRAINT: UNIQUE/.test(e.message) || + /unique constraint/.test(e.message) + ) { + throw new ConflictError(`Rejected due to a conflicting entity`, e); + } + + throw e; + } + } +} diff --git a/plugins/scaffolder-backend/src/tasks/types.ts b/plugins/scaffolder-backend/src/tasks/types.ts new file mode 100644 index 0000000000..27cbe7f7f1 --- /dev/null +++ b/plugins/scaffolder-backend/src/tasks/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ + +/** + * The core database implementation. + */ +export interface Database { + /** + * Runs a transaction. + * + * The callback is expected to make calls back into this class. When it + * completes, the transaction is closed. + * + * @param fn The callback that implements the transaction + */ + transaction(fn: (tx: Transaction) => Promise): Promise; +} + +/** + * An abstraction for transactions of the underlying database technology. + */ +export type Transaction = { + rollback(): Promise; +}; From 71dc78b01ff6d64c644da18986be7b5d450b8042 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 1 Feb 2021 14:06:09 +0100 Subject: [PATCH 031/158] Wire up database and rename database classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- packages/backend/src/plugins/scaffolder.ts | 2 + .../backend/src/plugins/scaffolder.ts | 2 + .../migrations/20210120143715_init.js | 14 +- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 248 ++++++++++++++++++ ...tabase.test.ts => MemoryTaskStore.test.ts} | 0 .../{MemoryDatabase.ts => MemoryTaskStore.ts} | 44 +--- ...oker.test.ts => StorageTaskBroker.test.ts} | 12 +- ...moryTaskBroker.ts => StorageTaskBroker.ts} | 18 +- .../src/scaffolder/tasks/TaskWorker.ts | 3 +- .../src/scaffolder/tasks/index.ts | 5 +- .../src/scaffolder/tasks/types.ts | 30 ++- .../scaffolder-backend/src/service/router.ts | 14 +- 12 files changed, 328 insertions(+), 64 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts rename plugins/scaffolder-backend/src/scaffolder/tasks/{MemoryDatabase.test.ts => MemoryTaskStore.test.ts} (100%) rename plugins/scaffolder-backend/src/scaffolder/tasks/{MemoryDatabase.ts => MemoryTaskStore.ts} (73%) rename plugins/scaffolder-backend/src/scaffolder/tasks/{MemoryTaskBroker.test.ts => StorageTaskBroker.test.ts} (90%) rename plugins/scaffolder-backend/src/scaffolder/tasks/{MemoryTaskBroker.ts => StorageTaskBroker.ts} (90%) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 4e2257a46c..723165ff82 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -30,6 +30,7 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); @@ -54,5 +55,6 @@ export default async function createPlugin({ config, dockerClient, entityClient, + database, }); } diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index c8bd3e5012..d68f90ce08 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -14,6 +14,7 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); @@ -38,5 +39,6 @@ export default async function createPlugin({ config, dockerClient, entityClient, + database, }); } diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js index d2535bfa86..bb65e1e2fb 100644 --- a/plugins/scaffolder-backend/migrations/20210120143715_init.js +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -23,10 +23,6 @@ exports.up = async function up(knex) { await knex.schema.createTable('tasks', table => { table.comment('The table of scaffolder tasks'); table.uuid('id').primary().notNullable().comment('The ID of the task'); - table - .text('context') - .notNullable() - .comment('A JSON object with task specific context information'); table .text('spec') .notNullable() @@ -41,6 +37,7 @@ exports.up = async function up(knex) { .comment('The current run ID of the task'); table .dateTime('created_at') + .defaultTo(knex.fn.now()) .notNullable() .comment('The timestamp when this task was created'); table @@ -53,6 +50,7 @@ exports.up = async function up(knex) { .defaultTo(0) .comment('The number of times that this task has been attempted'); }); + await knex.schema.createTable('task_events', table => { table.comment('The event stream a given task'); table @@ -72,16 +70,16 @@ exports.up = async function up(knex) { .nullable() .comment('The run ID of the task that this event applies to'); table - .text('stage_name') - .nullable() - .comment('The stage of the task that this event applies to'); + .text('body') + .notNullable() + .comment('The JSON encoded body of the event'); table.text('event_type').notNullable().comment('The type of event'); table .dateTime('created_at') .defaultTo(knex.fn.now()) .notNullable() .comment('The timestamp when this event was generated'); - table.text('text').notNullable().comment('The text of the event'); + table.index(['task_id'], 'task_events_task_id_idx'); }); }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts new file mode 100644 index 0000000000..e6de1735d8 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -0,0 +1,248 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { JsonObject } from '@backstage/config'; +import { + ConflictError, + NotFoundError, + resolvePackagePath, +} from '@backstage/backend-common'; +import Knex, { Transaction } from 'knex'; +import { Logger } from 'winston'; +import { v4 as uuid } from 'uuid'; +import { + DbTaskEventRow, + DbTaskRow, + Status, + TaskEventType, + TaskSpec, + TaskStore, + TaskStoreEmitOptions, + TaskStoreGetEventsOptions, +} from './types'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'migrations', +); + +export type RawDbTaskRow = { + id: string; + spec: string; + status: Status; + last_heartbeat_at?: string; + retry_count: number; + created_at: string; + run_id?: string; +}; + +export type RawDbTaskEventRow = { + id: number; + run_id: string; + task_id: string; + body: string; + event_type: TaskEventType; + created_at: string; +}; + +export class DatabaseTaskStore implements TaskStore { + static async create(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new DatabaseTaskStore(knex); + } + + constructor(private readonly db: Knex) {} + + async get(taskId: string): Promise { + const [result] = await this.db('tasks') + .where({ id: taskId }) + .select(); + if (!result) { + throw new NotFoundError(`No task with id '${taskId}' found`); + } + try { + const spec = JSON.parse(result.spec); + return { + id: result.id, + spec, + status: result.status, + lastHeartbeat: result.last_heartbeat_at, + retryCount: result.retry_count, + createdAt: result.created_at, + runId: result.run_id, + }; + } catch (error) { + throw new Error(`Failed to parse spec of task '${taskId}', ${error}`); + } + } + + async createTask(spec: TaskSpec): Promise<{ taskId: string }> { + const taskId = uuid(); + await this.db('tasks').insert({ + id: taskId, + spec: JSON.stringify(spec), + status: 'open', + retry_count: 0, + }); + return { taskId }; + } + + async claimTask(): Promise { + return this.db.transaction(async tx => { + const [task] = await tx('tasks') + .where({ + status: 'open', + }) + .limit(1) + .select(); + + if (!task) { + return undefined; + } + + const runId = uuid(); + const updateCount = await tx('tasks') + .where({ id: task.id, status: 'open' }) + .update({ + status: 'processing', + run_id: runId, + }); + + if (updateCount < 1) { + return undefined; + } + + try { + const spec = JSON.parse(task.spec); + return { + id: task.id, + spec, + status: 'processing', + lastHeartbeat: task.last_heartbeat_at, + retryCount: task.retry_count, + createdAt: task.created_at, + runId: runId, + }; + } catch (error) { + throw new Error(`Failed to parse spec of task '${task.id}', ${error}`); + } + }); + } + + async heartbeat(runId: string): Promise { + const updateCount = await this.db('tasks') + .where({ run_id: runId, status: 'processing' }) + .update({ + last_heartbeat_at: this.db.fn.now(), + }); + if (updateCount === 0) { + throw new Error(`No running task with runId ${runId} found`); + } + } + + async setStatus(runId: string, status: Status): Promise { + let oldStatus: string; + if (status === 'failed' || status === 'completed') { + oldStatus = 'processing'; + } else { + throw new Error( + `Invalid status update of run '${runId}' to status '${status}'`, + ); + } + await this.db.transaction(async tx => { + const [task] = await tx('tasks') + .where({ + run_id: runId, + }) + .limit(1) + .select(); + + if (!task) { + throw new Error(`No task with runId ${runId} found`); + } + if (task.status !== oldStatus) { + throw new ConflictError( + `Refusing to update status of run '${runId}' to status '${status}' ` + + `as it is currently '${task.status}', expected '${oldStatus}'`, + ); + } + const updateCount = await tx('tasks') + .where({ + run_id: runId, + status: oldStatus, + }) + .update({ + status, + }); + if (updateCount !== 1) { + throw new Error( + `Failed to update status to '${status}' for runId ${runId}`, + ); + } + }); + } + + async emit({ + taskId, + runId, + body, + type, + }: TaskStoreEmitOptions): Promise { + const serliazedBody = JSON.stringify(body); + await this.db('task_events').insert({ + task_id: taskId, + run_id: runId, + event_type: type, + body: serliazedBody, + }); + } + + async getEvents({ + taskId, + after, + }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { + let query = this.db('task_events').where({ + task_id: taskId, + }); + if (typeof after === 'number') { + query = query + .where('task_events.id', '>', after) + .orWhere({ event_type: 'completion' }); + } + + const rawEvents = await query.select(); + const events = rawEvents.map(event => { + try { + const body = JSON.parse(event.body) as JsonObject; + return { + id: event.id, + runId: event.run_id, + taskId: event.task_id, + body, + type: event.event_type, + createdAt: event.created_at, + }; + } catch (error) { + throw new Error( + `Failed to parse event body from event taskId=${taskId} id=${event.id}, ${error}`, + ); + } + }); + return { events }; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.test.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts similarity index 73% rename from plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts index b2d1cf667b..e181edf524 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryDatabase.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts @@ -19,35 +19,17 @@ import { DbTaskEventRow, Status, TaskSpec, - TaskEventType, + TaskStore, + TaskStoreGetEventsOptions, + TaskStoreEmitOptions, } from './types'; import { v4 as uuid } from 'uuid'; -export interface Database { - get(taskId: string): Promise; - createTask(task: TaskSpec): Promise; - claimTask(): Promise; - heartbeat(runId: string): Promise; - setStatus(taskId: string, status: Status): Promise; -} - -type EmitOptions = { - taskId: string; - runId: string; - body: string; - type: TaskEventType; -}; - -type ReadOptions = { - taskId: string; - after?: number | undefined; -}; - -export class MemoryDatabase implements Database { +export class MemoryTaskStore implements TaskStore { private readonly store = new Map(); private readonly events = new Array(); - async emit({ taskId, runId, body, type }: EmitOptions) { + async emit({ taskId, runId, body, type }: TaskStoreEmitOptions) { this.events.push({ id: this.events.length, taskId, @@ -61,7 +43,7 @@ export class MemoryDatabase implements Database { async getEvents({ taskId, after, - }: ReadOptions): Promise<{ events: DbTaskEventRow[] }> { + }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { const events = this.events.filter(event => { if (event.taskId !== taskId) { return false; @@ -89,7 +71,7 @@ export class MemoryDatabase implements Database { throw new Error('No task with matching runId found'); } - this.store.set(task.taskId, { + this.store.set(task.id, { ...task, lastHeartbeat: new Date().toISOString(), }); @@ -103,23 +85,23 @@ export class MemoryDatabase implements Database { status: 'processing', runId: uuid(), }; - this.store.set(t.taskId, task); + this.store.set(t.id, task); return task; } } return undefined; } - async createTask(spec: TaskSpec): Promise { + async createTask(spec: TaskSpec): Promise<{ taskId: string }> { const taskRow = { - taskId: uuid(), + id: uuid(), spec, status: 'open' as Status, retryCount: 0, createdAt: new Date().toISOString(), }; - this.store.set(taskRow.taskId, taskRow); - return taskRow; + this.store.set(taskRow.id, taskRow); + return { taskId: taskRow.id }; } async get(taskId: string): Promise { @@ -135,6 +117,6 @@ export class MemoryDatabase implements Database { if (!task) { throw new Error(`no task found`); } - this.store.set(task.taskId, { ...task, status }); + this.store.set(task.id, { ...task, status }); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts similarity index 90% rename from plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 686a6d9132..5bbe91ea87 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -15,13 +15,13 @@ */ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { TemplaterValues } from './actions/templater/types'; -import { MemoryDatabase } from './MemoryDatabase'; -import { MemoryTaskBroker, TaskAgent } from './MemoryTaskBroker'; +import { TemplaterValues } from '../stages'; +import { MemoryTaskStore } from './MemoryTaskStore'; +import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; -describe('MemoryTaskBroker', () => { - const storage = new MemoryDatabase(); - const broker = new MemoryTaskBroker(storage); +describe('StorageTaskBroker', () => { + const storage = new MemoryTaskStore(); + const broker = new StorageTaskBroker(storage); const taskSpec = { values: {} as TemplaterValues, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts similarity index 90% rename from plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 6937c535dc..6eedc023f8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -18,16 +18,16 @@ import { CompletedTaskState, Task, TaskSpec, + TaskStore, TaskBroker, DispatchResult, DbTaskEventRow, } from './types'; -import { MemoryDatabase } from './MemoryDatabase'; export class TaskAgent implements Task { private heartbeartInterval?: ReturnType; - static create(state: TaskState, storage: MemoryDatabase) { + static create(state: TaskState, storage: TaskStore) { const agent = new TaskAgent(state, storage); agent.start(); return agent; @@ -36,7 +36,7 @@ export class TaskAgent implements Task { // Runs heartbeat internally private constructor( private readonly state: TaskState, - private readonly storage: MemoryDatabase, + private readonly storage: TaskStore, ) {} get spec() { @@ -51,20 +51,20 @@ export class TaskAgent implements Task { await this.storage.emit({ taskId: this.state.taskId, runId: this.state.runId, - body: message, + body: { message }, type: 'log', }); } async complete(result: CompletedTaskState): Promise { await this.storage.setStatus( - this.state.taskId, + this.state.runId, result === 'failed' ? 'failed' : 'completed', ); this.storage.emit({ taskId: this.state.taskId, runId: this.state.runId, - body: `Run completed with status: ${result}`, + body: { message: `Run completed with status: ${result}` }, type: 'completion', }); if (this.heartbeartInterval) { @@ -96,8 +96,8 @@ function defer() { return { promise, resolve }; } -export class MemoryTaskBroker implements TaskBroker { - constructor(private readonly storage: MemoryDatabase) {} +export class StorageTaskBroker implements TaskBroker { + constructor(private readonly storage: TaskStore) {} private deferredDispatch = defer(); async claim(): Promise { @@ -107,7 +107,7 @@ export class MemoryTaskBroker implements TaskBroker { return TaskAgent.create( { runId: pendingTask.runId!, - taskId: pendingTask.taskId, + taskId: pendingTask.id, spec: pendingTask.spec, }, this.storage, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index c80a1b81bf..64cfdc6f96 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -73,6 +73,7 @@ export class TaskWorker { // Give us some time to curl observe task.emitLog('Task claimed, waiting ...'); await new Promise(resolve => setTimeout(resolve, 5000)); + console.log('DEBUG: task.spec =', JSON.stringify(task.spec, null, 2)); task.emitLog(`Starting up work with ${task.spec.steps.length} steps`); const outputs: { [name: string]: JsonValue } = {}; @@ -106,7 +107,7 @@ export class TaskWorker { await task.complete('completed'); } catch (error) { - task.emitLog(error); + task.emitLog(String(error.stack)); await task.complete('failed'); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 1cc9842fed..7b6552a3e0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export { MemoryDatabase } from './MemoryDatabase'; -export { MemoryTaskBroker } from './MemoryTaskBroker'; +export { MemoryTaskStore } from './MemoryTaskStore'; +export { DatabaseTaskStore } from './DatabaseTaskStore'; +export { StorageTaskBroker } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index d3c08085fd..f9081af6b6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue, JsonObject } from '@backstage/config'; export type Status = | 'open' @@ -26,7 +26,7 @@ export type Status = export type CompletedTaskState = 'failed' | 'completed'; export type DbTaskRow = { - taskId: string; + id: string; spec: TaskSpec; status: Status; lastHeartbeat?: string; @@ -40,7 +40,7 @@ export type DbTaskEventRow = { id: number; runId: string; taskId: string; - body: string; + body: JsonObject; type: TaskEventType; createdAt: string; }; @@ -69,3 +69,27 @@ export interface TaskBroker { claim(): Promise; dispatch(spec: TaskSpec): Promise; } + +export type TaskStoreEmitOptions = { + taskId: string; + runId: string; + body: JsonObject; + type: TaskEventType; +}; + +export type TaskStoreGetEventsOptions = { + taskId: string; + after?: number | undefined; +}; +export interface TaskStore { + get(taskId: string): Promise; + createTask(task: TaskSpec): Promise<{ taskId: string }>; + claimTask(): Promise; + heartbeat(runId: string): Promise; + setStatus(runId: string, status: Status): Promise; + emit({ taskId, runId, body, type }: TaskStoreEmitOptions): Promise; + getEvents({ + taskId, + after, + }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }>; +} diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index a72a5c5d48..23af538df8 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -34,17 +34,17 @@ import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; import parseGitUrl from 'git-url-parse'; import { - MemoryTaskBroker, - MemoryDatabase, + DatabaseTaskStore, + StorageTaskBroker, TaskWorker, } from '../scaffolder/tasks'; import { TemplateActionRegistry, templateEntityToSpec, } from '../scaffolder/tasks/TemplateConverter'; -import { LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { registerLegacyActions } from '../scaffolder/stages/legacy'; import { getWorkingDirectory } from './helpers'; +import { PluginDatabaseManager } from '@backstage/backend-common'; export interface RouterOptions { preparers: PreparerBuilder; @@ -55,6 +55,7 @@ export interface RouterOptions { config: Config; dockerClient: Docker; entityClient: CatalogEntityClient; + database: PluginDatabaseManager; } export async function createRouter( @@ -71,12 +72,17 @@ export async function createRouter( config, dockerClient, entityClient, + database, } = options; const logger = parentLogger.child({ plugin: 'scaffolder' }); const workingDirectory = await getWorkingDirectory(config, logger); const jobProcessor = await JobProcessor.fromConfig({ config, logger }); - const taskBroker = new MemoryTaskBroker(new MemoryDatabase()); + + const databaseTaskStore = await DatabaseTaskStore.create( + await database.getClient(), + ); + const taskBroker = new StorageTaskBroker(databaseTaskStore); const actionRegistry = new TemplateActionRegistry(); const worker = new TaskWorker({ logger, From 5da5e6b2243516a448fbbe88fd806b550ba2f0ed Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 1 Feb 2021 14:07:19 +0100 Subject: [PATCH 032/158] Delete MemoryTaskStore This is replaced by running sqlite in memory. --- .../scaffolder/tasks/MemoryTaskStore.test.ts | 21 --- .../src/scaffolder/tasks/MemoryTaskStore.ts | 122 ------------------ .../src/scaffolder/tasks/index.ts | 1 - 3 files changed, 144 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.test.ts deleted file mode 100644 index c5522cb38a..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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. - */ - -describe('MemoryDatabase', () => { - it('should be tested', async () => { - expect(1).toBe(2); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts deleted file mode 100644 index e181edf524..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/MemoryTaskStore.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { - DbTaskRow, - DbTaskEventRow, - Status, - TaskSpec, - TaskStore, - TaskStoreGetEventsOptions, - TaskStoreEmitOptions, -} from './types'; -import { v4 as uuid } from 'uuid'; - -export class MemoryTaskStore implements TaskStore { - private readonly store = new Map(); - private readonly events = new Array(); - - async emit({ taskId, runId, body, type }: TaskStoreEmitOptions) { - this.events.push({ - id: this.events.length, - taskId, - runId, - body, - type, - createdAt: new Date().toISOString(), - }); - } - - async getEvents({ - taskId, - after, - }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { - const events = this.events.filter(event => { - if (event.taskId !== taskId) { - return false; - } - if (after !== undefined) { - if (event.id <= after) { - return false; - } - } - return true; - }); - return { events }; - } - - async heartbeat(runId: string): Promise { - let task: DbTaskRow | undefined; - - for (const t of this.store.values()) { - if (t.runId === runId) { - task = t; - } - } - - if (!task) { - throw new Error('No task with matching runId found'); - } - - this.store.set(task.id, { - ...task, - lastHeartbeat: new Date().toISOString(), - }); - } - - async claimTask(): Promise { - for (const t of this.store.values()) { - if (t.status === 'open') { - const task: DbTaskRow = { - ...t, - status: 'processing', - runId: uuid(), - }; - this.store.set(t.id, task); - return task; - } - } - return undefined; - } - - async createTask(spec: TaskSpec): Promise<{ taskId: string }> { - const taskRow = { - id: uuid(), - spec, - status: 'open' as Status, - retryCount: 0, - createdAt: new Date().toISOString(), - }; - this.store.set(taskRow.id, taskRow); - return { taskId: taskRow.id }; - } - - async get(taskId: string): Promise { - const task = this.store.get(taskId); - if (task) { - return task; - } - throw new Error(`could not found task ${taskId}`); - } - - async setStatus(taskId: string, status: Status): Promise { - const task = this.store.get(taskId); - if (!task) { - throw new Error(`no task found`); - } - this.store.set(task.id, { ...task, status }); - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 7b6552a3e0..c85135426e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -export { MemoryTaskStore } from './MemoryTaskStore'; export { DatabaseTaskStore } from './DatabaseTaskStore'; export { StorageTaskBroker } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; From 88c31ab16ba778b43c5c0e6fb6aad4a3f68cf8c0 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 2 Feb 2021 09:27:18 +0100 Subject: [PATCH 033/158] Add tests. Extend storage with fetch staletasks method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- plugins/scaffolder-backend/package.json | 3 +- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 35 +++-- .../tasks/StorageTaskBroker.test.ts | 145 ++++++++++++++---- .../src/scaffolder/tasks/StorageTaskBroker.ts | 17 +- .../src/scaffolder/tasks/types.ts | 1 + .../src/service/router.test.ts | 25 ++- .../scaffolder-backend/src/service/router.ts | 6 +- 7 files changed, 182 insertions(+), 50 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 5d024c2798..bdecf6196e 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -58,7 +58,8 @@ "p-queue": "^6.3.0", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0" + "yaml": "^1.10.0", + "luxon": "^1.25.0" }, "devDependencies": { "@backstage/cli": "^0.5.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index e6de1735d8..1d99cd2958 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -20,8 +20,7 @@ import { NotFoundError, resolvePackagePath, } from '@backstage/backend-common'; -import Knex, { Transaction } from 'knex'; -import { Logger } from 'winston'; +import Knex from 'knex'; import { v4 as uuid } from 'uuid'; import { DbTaskEventRow, @@ -121,6 +120,7 @@ export class DatabaseTaskStore implements TaskStore { .update({ status: 'processing', run_id: runId, + last_heartbeat_at: this.db.fn.now(), }); if (updateCount < 1) { @@ -155,6 +155,18 @@ export class DatabaseTaskStore implements TaskStore { } } + async listStaleTasks(): Promise<{ tasks: DbTaskRow }> { + const rows = await this.db('tasks') + .where('status', 'processing') + .andWhere( + 'last_heartbeat_at', + '<', + this.db.type === 'sqlite' + ? this.db.raw("datetime('now', '-2 seconds')") + : this.db.raw("dateadd('second', -2, now())"), + ); + } + async setStatus(runId: string, status: Status): Promise { let oldStatus: string; if (status === 'failed' || status === 'completed') { @@ -216,16 +228,17 @@ export class DatabaseTaskStore implements TaskStore { taskId, after, }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { - let query = this.db('task_events').where({ - task_id: taskId, - }); - if (typeof after === 'number') { - query = query - .where('task_events.id', '>', after) - .orWhere({ event_type: 'completion' }); - } + const rawEvents = await this.db('task_events') + .where({ + task_id: taskId, + }) + .andWhere(builder => { + if (typeof after === 'number') { + builder.where('id', '>', after).orWhere('event_type', 'completion'); + } + }) + .select(); - const rawEvents = await query.select(); const events = rawEvents.map(event => { try { const body = JSON.parse(event.body) as JsonObject; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 5bbe91ea87..ebbac1db5f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -14,47 +14,54 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { TemplaterValues } from '../stages'; -import { MemoryTaskStore } from './MemoryTaskStore'; +import { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; +import { TaskStore, TaskSpec, DbTaskEventRow } from './types'; + +async function createStore(): Promise { + const manager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('scaffolder'); + return await DatabaseTaskStore.create(await manager.getClient()); +} describe('StorageTaskBroker', () => { - const storage = new MemoryTaskStore(); - const broker = new StorageTaskBroker(storage); + let storage: TaskStore; - const taskSpec = { - values: {} as TemplaterValues, - template: {} as TemplateEntityV1alpha1, - }; + beforeAll(async () => { + storage = await createStore(); + }); it('should claim a dispatched work item', async () => { - await broker.dispatch(taskSpec); + const broker = new StorageTaskBroker(storage); + await broker.dispatch({ steps: [] }); await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); }); it('should wait for a dispatched work item', async () => { + const broker = new StorageTaskBroker(storage); const promise = broker.claim(); await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); - await broker.dispatch(taskSpec); + await broker.dispatch({ steps: [] }); await expect(promise).resolves.toEqual(expect.any(TaskAgent)); }); it('should dispatch multiple items and claim them in order', async () => { - await broker.dispatch({ - values: { owner: 'a' } as TemplaterValues, - template: {} as TemplateEntityV1alpha1, - }); - await broker.dispatch({ - values: { owner: 'b' } as TemplaterValues, - template: {} as TemplateEntityV1alpha1, - }); - await broker.dispatch({ - values: { owner: 'c' } as TemplaterValues, - template: {} as TemplateEntityV1alpha1, - }); + const broker = new StorageTaskBroker(storage); + await broker.dispatch({ steps: [{ id: 'a' }] } as TaskSpec); + await broker.dispatch({ steps: [{ id: 'b' }] } as TaskSpec); + await broker.dispatch({ steps: [{ id: 'c' }] } as TaskSpec); const taskA = await broker.claim(); const taskB = await broker.claim(); @@ -62,13 +69,14 @@ describe('StorageTaskBroker', () => { await expect(taskA).toEqual(expect.any(TaskAgent)); await expect(taskB).toEqual(expect.any(TaskAgent)); await expect(taskC).toEqual(expect.any(TaskAgent)); - await expect(taskA.spec.values.owner).toBe('a'); - await expect(taskB.spec.values.owner).toBe('b'); - await expect(taskC.spec.values.owner).toBe('c'); + await expect(taskA.spec.steps[0].id).toBe('a'); + await expect(taskB.spec.steps[0].id).toBe('b'); + await expect(taskC.spec.steps[0].id).toBe('c'); }); it('should complete a task', async () => { - const dispatchResult = await broker.dispatch(taskSpec); + const broker = new StorageTaskBroker(storage); + const dispatchResult = await broker.dispatch({ steps: [] }); const task = await broker.claim(); await task.complete('completed'); const taskRow = await storage.get(dispatchResult.taskId); @@ -76,10 +84,91 @@ describe('StorageTaskBroker', () => { }); it('should fail a task', async () => { - const dispatchResult = await broker.dispatch(taskSpec); + const broker = new StorageTaskBroker(storage); + const dispatchResult = await broker.dispatch({ steps: [] }); const task = await broker.claim(); await task.complete('failed'); const taskRow = await storage.get(dispatchResult.taskId); expect(taskRow.status).toBe('failed'); }); + + it('multiple brokers should be able to observe a single task', async () => { + const broker1 = new StorageTaskBroker(storage); + const broker2 = new StorageTaskBroker(storage); + + const { taskId } = await broker1.dispatch({ steps: [] }); + + const logPromise = new Promise(resolve => { + const observedEvents = new Array(); + + broker2.observe({ taskId, after: undefined }, (_err, { events }) => { + observedEvents.push(...events); + if (events.some(e => e.type === 'completion')) { + resolve(observedEvents); + } + }); + }); + const task = await broker1.claim(); + await task.emitLog('log 1'); + await task.emitLog('log 2'); + await task.emitLog('log 3'); + await task.complete('completed'); + + const logs = await logPromise; + expect(logs.map(l => l.body.message)).toEqual([ + 'log 1', + 'log 2', + 'log 3', + 'Run completed with status: completed', + ]); + + const afterLogs = await new Promise(resolve => { + broker2.observe({ taskId, after: logs[1].id }, (_err, { events }) => + resolve(events.map(e => e.body.message as string)), + ); + }); + expect(afterLogs).toEqual([ + 'log 3', + 'Run completed with status: completed', + ]); + }); + + it('should heartbeat', async () => { + const broker = new StorageTaskBroker(storage); + const { taskId } = await broker.dispatch({ steps: [] }); + const task = await broker.claim(); + + const initialTask = await storage.get(taskId); + + for (;;) { + const maybeTask = await storage.get(taskId); + if (maybeTask.lastHeartbeat !== initialTask.lastHeartbeat) { + break; + } + await new Promise(resolve => setTimeout(resolve, 50)); + } + await task.complete('completed'); + expect.assertions(0); + }); + + it('should be cancelled if heartbeat stops', async () => { + const broker = new StorageTaskBroker(storage); + const { taskId } = await broker.dispatch({ steps: [] }); + console.log('DEBUG: taskId =', taskId); + const task = await broker.claim(); + clearInterval((task as any).heartbeatInterval); + + setTimeout(() => { + storage.listStaleTasks(); + }, 4000); + + for (;;) { + const maybeTask = await storage.get(taskId); + if (maybeTask.status === 'cancelled') { + break; + } + await new Promise(resolve => setTimeout(resolve, 50)); + } + expect.assertions(0); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 6eedc023f8..8c0d63ee71 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -25,7 +25,7 @@ import { } from './types'; export class TaskAgent implements Task { - private heartbeartInterval?: ReturnType; + private heartbeatInterval?: ReturnType; static create(state: TaskState, storage: TaskStore) { const agent = new TaskAgent(state, storage); @@ -67,13 +67,13 @@ export class TaskAgent implements Task { body: { message: `Run completed with status: ${result}` }, type: 'completion', }); - if (this.heartbeartInterval) { - clearInterval(this.heartbeartInterval); + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); } } private start() { - this.heartbeartInterval = setInterval(() => { + this.heartbeatInterval = setInterval(() => { if (!this.state.runId) { throw new Error('no run id provided'); } @@ -131,7 +131,10 @@ export class StorageTaskBroker implements TaskBroker { taskId: string; after: number | undefined; }, - callback: (result: { events: DbTaskEventRow[] }) => void, + callback: ( + error: Error | undefined, + result: { events: DbTaskEventRow[] }, + ) => void, ): () => void { const { taskId } = options; @@ -148,9 +151,9 @@ export class StorageTaskBroker implements TaskBroker { if (events.length) { after = events[events.length - 1].id; try { - callback(result); + callback(undefined, result); } catch (error) { - console.log('DEBUG: error =', error); + callback(error, { events: [] }); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index f9081af6b6..dd9a1ad9ad 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -86,6 +86,7 @@ export interface TaskStore { createTask(task: TaskSpec): Promise<{ taskId: string }>; claimTask(): Promise; heartbeat(runId: string): Promise; + listStaleTasks(): Promise<{ tasks: DbTaskRow }>; setStatus(runId: string, status: Status): Promise; emit({ taskId, runId, body, type }: TaskStoreEmitOptions): Promise; getEvents({ diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 31f62febd2..8a0fa6cba6 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -16,6 +16,7 @@ const mockAccess = jest.fn(); jest.doMock('fs-extra', () => ({ + access: mockAccess, promises: { access: mockAccess, }, @@ -27,7 +28,11 @@ jest.doMock('fs-extra', () => ({ remove: jest.fn(), })); -import { getVoidLogger } from '@backstage/backend-common'; +import { + SingleConnectionDatabaseManager, + PluginDatabaseManager, + getVoidLogger, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; @@ -41,6 +46,19 @@ const generateEntityClient: any = (template: any) => ({ findTemplate: () => Promise.resolve(template), }); +function createDatabase(): PluginDatabaseManager { + return SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('scaffolder'); +} + describe('createRouter - working directory', () => { const mockPrepare = jest.fn(); const mockPreparers = new Preparers(); @@ -78,7 +96,6 @@ describe('createRouter - working directory', () => { }; const mockedEntityClient = generateEntityClient(template); - it('should throw an error when working directory does not exist or is not writable', async () => { mockAccess.mockImplementation(() => { throw new Error('access error'); @@ -93,6 +110,7 @@ describe('createRouter - working directory', () => { config: new ConfigReader(workDirConfig('/path')), dockerClient: new Docker(), entityClient: mockedEntityClient, + database: createDatabase(), }), ).rejects.toThrow('access error'); }); @@ -106,6 +124,7 @@ describe('createRouter - working directory', () => { config: new ConfigReader(workDirConfig('/path')), dockerClient: new Docker(), entityClient: mockedEntityClient, + database: createDatabase(), }); const app = express().use(router); @@ -134,6 +153,7 @@ describe('createRouter - working directory', () => { config: new ConfigReader({}), dockerClient: new Docker(), entityClient: mockedEntityClient, + database: createDatabase(), }); const app = express().use(router); @@ -203,6 +223,7 @@ describe('createRouter', () => { config: new ConfigReader({}), dockerClient: new Docker(), entityClient: generateEntityClient(template), + database: createDatabase(), }); app = express().use(router); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 23af538df8..6eb112fa3c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -164,7 +164,11 @@ export async function createRouter( // After client opens connection send all nests as string const unsubscribe = taskBroker.observe( { taskId, after }, - ({ events }) => { + (error, { events }) => { + logger.error( + `Received error from event stream when observing task ${taskId}`, + error, + ); for (const event of events) { res.write(`event:${JSON.stringify(event)}\n\n`); if (event.type === 'completion') { From 6608f9f5ee4cab89e5f658924db7b3ed975ff712 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 2 Feb 2021 16:10:13 +0100 Subject: [PATCH 034/158] Drop runId, update TaskStore interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .../migrations/20210120143715_init.js | 17 +--- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 85 +++++++++--------- .../tasks/StorageTaskBroker.test.ts | 57 ++++++------ .../src/scaffolder/tasks/StorageTaskBroker.ts | 86 +++++++++++++------ .../src/scaffolder/tasks/TaskWorker.ts | 1 - .../src/scaffolder/tasks/types.ts | 39 ++++++--- .../scaffolder-backend/src/service/router.ts | 4 +- 7 files changed, 168 insertions(+), 121 deletions(-) diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js index bb65e1e2fb..248b4f932f 100644 --- a/plugins/scaffolder-backend/migrations/20210120143715_init.js +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -31,10 +31,6 @@ exports.up = async function up(knex) { .text('status') .notNullable() .comment('The current status of the task'); - table - .integer('run_id') - .nullable() - .comment('The current run ID of the task'); table .dateTime('created_at') .defaultTo(knex.fn.now()) @@ -44,11 +40,6 @@ exports.up = async function up(knex) { .dateTime('last_heartbeat_at') .nullable() .comment('The last timestamp when a heartbeat was received'); - table - .integer('retry_count') - .notNullable() - .defaultTo(0) - .comment('The number of times that this task has been attempted'); }); await knex.schema.createTable('task_events', table => { @@ -65,17 +56,13 @@ exports.up = async function up(knex) { .notNullable() .onDelete('CASCADE') .comment('The task that generated the event'); - table - .integer('run_id') - .nullable() - .comment('The run ID of the task that this event applies to'); table .text('body') .notNullable() .comment('The JSON encoded body of the event'); table.text('event_type').notNullable().comment('The type of event'); table - .dateTime('created_at') + .timestamp('created_at', { precision: 9 }) .defaultTo(knex.fn.now()) .notNullable() .comment('The timestamp when this event was generated'); @@ -90,7 +77,7 @@ exports.up = async function up(knex) { exports.down = async function down(knex) { if (knex.client.config.client !== 'sqlite3') { await knex.schema.alterTable('task_events', table => { - table.dropIndex([], 'task_events_task_id_idx'); + table.dropIndex([], 'ctask_events_task_id_idx'); }); } await knex.schema.dropTable('task_events'); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 1d99cd2958..cf1bc18621 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -43,14 +43,11 @@ export type RawDbTaskRow = { spec: string; status: Status; last_heartbeat_at?: string; - retry_count: number; created_at: string; - run_id?: string; }; export type RawDbTaskEventRow = { id: number; - run_id: string; task_id: string; body: string; event_type: TaskEventType; @@ -80,10 +77,8 @@ export class DatabaseTaskStore implements TaskStore { id: result.id, spec, status: result.status, - lastHeartbeat: result.last_heartbeat_at, - retryCount: result.retry_count, + lastHeartbeatAt: result.last_heartbeat_at, createdAt: result.created_at, - runId: result.run_id, }; } catch (error) { throw new Error(`Failed to parse spec of task '${taskId}', ${error}`); @@ -96,7 +91,6 @@ export class DatabaseTaskStore implements TaskStore { id: taskId, spec: JSON.stringify(spec), status: 'open', - retry_count: 0, }); return { taskId }; } @@ -114,12 +108,10 @@ export class DatabaseTaskStore implements TaskStore { return undefined; } - const runId = uuid(); const updateCount = await tx('tasks') .where({ id: task.id, status: 'open' }) .update({ status: 'processing', - run_id: runId, last_heartbeat_at: this.db.fn.now(), }); @@ -133,10 +125,8 @@ export class DatabaseTaskStore implements TaskStore { id: task.id, spec, status: 'processing', - lastHeartbeat: task.last_heartbeat_at, - retryCount: task.retry_count, + lastHeartbeatAt: task.last_heartbeat_at, createdAt: task.created_at, - runId: runId, }; } catch (error) { throw new Error(`Failed to parse spec of task '${task.id}', ${error}`); @@ -144,58 +134,76 @@ export class DatabaseTaskStore implements TaskStore { }); } - async heartbeat(runId: string): Promise { + async heartbeatTask(taskId: string): Promise { const updateCount = await this.db('tasks') - .where({ run_id: runId, status: 'processing' }) + .where({ id: taskId, status: 'processing' }) .update({ last_heartbeat_at: this.db.fn.now(), }); if (updateCount === 0) { - throw new Error(`No running task with runId ${runId} found`); + throw new Error(`No running task with taskId ${taskId} found`); } } - async listStaleTasks(): Promise<{ tasks: DbTaskRow }> { - const rows = await this.db('tasks') + async listStaleTasks({ + timeoutS, + }: { + timeoutS: number; + }): Promise<{ + tasks: { taskId: string }[]; + }> { + const rawRows = await this.db('tasks') .where('status', 'processing') .andWhere( 'last_heartbeat_at', - '<', - this.db.type === 'sqlite' - ? this.db.raw("datetime('now', '-2 seconds')") - : this.db.raw("dateadd('second', -2, now())"), + '<=', + this.db.client.config.client === 'sqlite3' + ? this.db.raw(`datetime('now', '-${Number(timeoutS)} seconds')`) + : this.db.raw(`dateadd('second', -${Number(timeoutS)}, now())`), ); + const tasks = rawRows.map(row => ({ + taskId: row.id, + })); + return { tasks }; } - async setStatus(runId: string, status: Status): Promise { + async completeTask({ + taskId, + status, + eventBody, + }: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise { let oldStatus: string; if (status === 'failed' || status === 'completed') { oldStatus = 'processing'; } else { throw new Error( - `Invalid status update of run '${runId}' to status '${status}'`, + `Invalid status update of run '${taskId}' to status '${status}'`, ); } await this.db.transaction(async tx => { const [task] = await tx('tasks') .where({ - run_id: runId, + id: taskId, }) .limit(1) .select(); if (!task) { - throw new Error(`No task with runId ${runId} found`); + throw new Error(`No task with taskId ${taskId} found`); } if (task.status !== oldStatus) { throw new ConflictError( - `Refusing to update status of run '${runId}' to status '${status}' ` + + `Refusing to update status of run '${taskId}' to status '${status}' ` + `as it is currently '${task.status}', expected '${oldStatus}'`, ); } const updateCount = await tx('tasks') .where({ - run_id: runId, + id: taskId, status: oldStatus, }) .update({ @@ -203,28 +211,28 @@ export class DatabaseTaskStore implements TaskStore { }); if (updateCount !== 1) { throw new Error( - `Failed to update status to '${status}' for runId ${runId}`, + `Failed to update status to '${status}' for taskId ${taskId}`, ); } + + await tx('task_events').insert({ + task_id: taskId, + event_type: 'completion', + body: JSON.stringify(eventBody), + }); }); } - async emit({ - taskId, - runId, - body, - type, - }: TaskStoreEmitOptions): Promise { + async emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise { const serliazedBody = JSON.stringify(body); await this.db('task_events').insert({ task_id: taskId, - run_id: runId, - event_type: type, + event_type: 'log', body: serliazedBody, }); } - async getEvents({ + async listEvents({ taskId, after, }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { @@ -244,8 +252,7 @@ export class DatabaseTaskStore implements TaskStore { const body = JSON.parse(event.body) as JsonObject; return { id: event.id, - runId: event.run_id, - taskId: event.task_id, + taskId, body, type: event.event_type, createdAt: event.created_at, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index ebbac1db5f..c45fa46335 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -14,13 +14,16 @@ * limitations under the License. */ -import { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { + getVoidLogger, + SingleConnectionDatabaseManager, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; -import { TaskStore, TaskSpec, DbTaskEventRow } from './types'; +import { TaskSpec, DbTaskEventRow } from './types'; -async function createStore(): Promise { +async function createStore(): Promise { const manager = SingleConnectionDatabaseManager.fromConfig( new ConfigReader({ backend: { @@ -35,20 +38,21 @@ async function createStore(): Promise { } describe('StorageTaskBroker', () => { - let storage: TaskStore; + let storage: DatabaseTaskStore; beforeAll(async () => { storage = await createStore(); }); + const logger = getVoidLogger(); it('should claim a dispatched work item', async () => { - const broker = new StorageTaskBroker(storage); + const broker = new StorageTaskBroker(storage, logger); await broker.dispatch({ steps: [] }); await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); }); it('should wait for a dispatched work item', async () => { - const broker = new StorageTaskBroker(storage); + const broker = new StorageTaskBroker(storage, logger); const promise = broker.claim(); await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); @@ -58,7 +62,7 @@ describe('StorageTaskBroker', () => { }); it('should dispatch multiple items and claim them in order', async () => { - const broker = new StorageTaskBroker(storage); + const broker = new StorageTaskBroker(storage, logger); await broker.dispatch({ steps: [{ id: 'a' }] } as TaskSpec); await broker.dispatch({ steps: [{ id: 'b' }] } as TaskSpec); await broker.dispatch({ steps: [{ id: 'c' }] } as TaskSpec); @@ -75,16 +79,16 @@ describe('StorageTaskBroker', () => { }); it('should complete a task', async () => { - const broker = new StorageTaskBroker(storage); + const broker = new StorageTaskBroker(storage, logger); const dispatchResult = await broker.dispatch({ steps: [] }); const task = await broker.claim(); await task.complete('completed'); const taskRow = await storage.get(dispatchResult.taskId); expect(taskRow.status).toBe('completed'); - }); + }, 10000); it('should fail a task', async () => { - const broker = new StorageTaskBroker(storage); + const broker = new StorageTaskBroker(storage, logger); const dispatchResult = await broker.dispatch({ steps: [] }); const task = await broker.claim(); await task.complete('failed'); @@ -93,8 +97,8 @@ describe('StorageTaskBroker', () => { }); it('multiple brokers should be able to observe a single task', async () => { - const broker1 = new StorageTaskBroker(storage); - const broker2 = new StorageTaskBroker(storage); + const broker1 = new StorageTaskBroker(storage, logger); + const broker2 = new StorageTaskBroker(storage, logger); const { taskId } = await broker1.dispatch({ steps: [] }); @@ -115,7 +119,7 @@ describe('StorageTaskBroker', () => { await task.complete('completed'); const logs = await logPromise; - expect(logs.map(l => l.body.message)).toEqual([ + expect(logs.map(l => l.body.message, logger)).toEqual([ 'log 1', 'log 2', 'log 3', @@ -134,7 +138,7 @@ describe('StorageTaskBroker', () => { }); it('should heartbeat', async () => { - const broker = new StorageTaskBroker(storage); + const broker = new StorageTaskBroker(storage, logger); const { taskId } = await broker.dispatch({ steps: [] }); const task = await broker.claim(); @@ -142,7 +146,7 @@ describe('StorageTaskBroker', () => { for (;;) { const maybeTask = await storage.get(taskId); - if (maybeTask.lastHeartbeat !== initialTask.lastHeartbeat) { + if (maybeTask.lastHeartbeatAt !== initialTask.lastHeartbeatAt) { break; } await new Promise(resolve => setTimeout(resolve, 50)); @@ -151,24 +155,29 @@ describe('StorageTaskBroker', () => { expect.assertions(0); }); - it('should be cancelled if heartbeat stops', async () => { - const broker = new StorageTaskBroker(storage); + it('should be update the status to failed if heartbeat fails', async () => { + const broker = new StorageTaskBroker(storage, logger); const { taskId } = await broker.dispatch({ steps: [] }); - console.log('DEBUG: taskId =', taskId); const task = await broker.claim(); - clearInterval((task as any).heartbeatInterval); - setTimeout(() => { - storage.listStaleTasks(); - }, 4000); + jest + .spyOn((task as any).storage, 'heartbeatTask') + .mockRejectedValue(new Error('nah m8')); + + const intervalId = setInterval(() => { + broker.vacuumTasks({ timeoutS: 2 }).catch(fail); + }, 500); for (;;) { const maybeTask = await storage.get(taskId); - if (maybeTask.status === 'cancelled') { + if (maybeTask.status === 'failed') { break; } await new Promise(resolve => setTimeout(resolve, 50)); } - expect.assertions(0); + + clearInterval(intervalId); + + expect(task.done).toBe(true); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 8c0d63ee71..48c0d5e5ba 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { Logger } from 'winston'; import { CompletedTaskState, Task, @@ -25,11 +25,13 @@ import { } from './types'; export class TaskAgent implements Task { - private heartbeatInterval?: ReturnType; + private isDone = false; - static create(state: TaskState, storage: TaskStore) { - const agent = new TaskAgent(state, storage); - agent.start(); + private heartbeatTimeoutId?: ReturnType; + + static create(state: TaskState, storage: TaskStore, logger: Logger) { + const agent = new TaskAgent(state, storage, logger); + agent.startTimeout(); return agent; } @@ -37,6 +39,7 @@ export class TaskAgent implements Task { private constructor( private readonly state: TaskState, private readonly storage: TaskStore, + private readonly logger: Logger, ) {} get spec() { @@ -44,40 +47,45 @@ export class TaskAgent implements Task { } async getWorkspaceName() { - return `${this.state.taskId}_${this.state.runId}`; + return this.state.taskId; + } + + get done() { + return this.isDone; } async emitLog(message: string): Promise { - await this.storage.emit({ + await this.storage.emitLogEvent({ taskId: this.state.taskId, - runId: this.state.runId, body: { message }, - type: 'log', }); } async complete(result: CompletedTaskState): Promise { - await this.storage.setStatus( - this.state.runId, - result === 'failed' ? 'failed' : 'completed', - ); - this.storage.emit({ + await this.storage.completeTask({ taskId: this.state.taskId, - runId: this.state.runId, - body: { message: `Run completed with status: ${result}` }, - type: 'completion', + status: result === 'failed' ? 'failed' : 'completed', + eventBody: { message: `Run completed with status: ${result}` }, }); - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); + this.isDone = true; + if (this.heartbeatTimeoutId) { + clearTimeout(this.heartbeatTimeoutId); } } - private start() { - this.heartbeatInterval = setInterval(() => { - if (!this.state.runId) { - throw new Error('no run id provided'); + private startTimeout() { + this.heartbeatTimeoutId = setTimeout(async () => { + try { + await this.storage.heartbeatTask(this.state.taskId); + this.startTimeout(); + } catch (error) { + this.isDone = true; + + this.logger.error( + `Heartbeat for task ${this.state.taskId} failed`, + error, + ); } - this.storage.heartbeat(this.state.runId); }, 1000); } } @@ -85,7 +93,6 @@ export class TaskAgent implements Task { interface TaskState { spec: TaskSpec; taskId: string; - runId: string; } function defer() { @@ -97,7 +104,10 @@ function defer() { } export class StorageTaskBroker implements TaskBroker { - constructor(private readonly storage: TaskStore) {} + constructor( + private readonly storage: TaskStore, + private readonly logger: Logger, + ) {} private deferredDispatch = defer(); async claim(): Promise { @@ -106,11 +116,11 @@ export class StorageTaskBroker implements TaskBroker { if (pendingTask) { return TaskAgent.create( { - runId: pendingTask.runId!, taskId: pendingTask.id, spec: pendingTask.spec, }, this.storage, + this.logger, ); } @@ -146,7 +156,7 @@ export class StorageTaskBroker implements TaskBroker { (async () => { let after = options.after; while (!cancelled) { - const result = await this.storage.getEvents({ taskId, after: after }); + const result = await this.storage.listEvents({ taskId, after: after }); const { events } = result; if (events.length) { after = events[events.length - 1].id; @@ -164,6 +174,26 @@ export class StorageTaskBroker implements TaskBroker { return unsubscribe; } + async vacuumTasks(timeoutS: { timeoutS: number }): Promise { + const { tasks } = await this.storage.listStaleTasks(timeoutS); + await Promise.all( + tasks.map(async task => { + try { + await this.storage.completeTask({ + taskId: task.taskId, + status: 'failed', + eventBody: { + message: + 'The task was cancelled because the task worker lost connection to the task broker', + }, + }); + } catch (error) { + this.logger.warn(`Failed to cancel task '${task.taskId}', ${error}`); + } + }), + ); + } + private waitForDispatch() { return this.deferredDispatch.promise; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 64cfdc6f96..f87d2df934 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -73,7 +73,6 @@ export class TaskWorker { // Give us some time to curl observe task.emitLog('Task claimed, waiting ...'); await new Promise(resolve => setTimeout(resolve, 5000)); - console.log('DEBUG: task.spec =', JSON.stringify(task.spec, null, 2)); task.emitLog(`Starting up work with ${task.spec.steps.length} steps`); const outputs: { [name: string]: JsonValue } = {}; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index dd9a1ad9ad..0c2592109b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -29,16 +29,13 @@ export type DbTaskRow = { id: string; spec: TaskSpec; status: Status; - lastHeartbeat?: string; - retryCount: number; createdAt: string; - runId?: string; + lastHeartbeatAt?: string; }; export type TaskEventType = 'completion' | 'log'; export type DbTaskEventRow = { id: number; - runId: string; taskId: string; body: JsonObject; type: TaskEventType; @@ -60,6 +57,7 @@ export type DispatchResult = { export interface Task { spec: TaskSpec; + done: boolean; emitLog(message: string): Promise; complete(result: CompletedTaskState): Promise; getWorkspaceName(): Promise; @@ -68,13 +66,22 @@ export interface Task { export interface TaskBroker { claim(): Promise; dispatch(spec: TaskSpec): Promise; + vacuumTasks(timeoutS: { timeoutS: number }): Promise; + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: ( + error: Error | undefined, + result: { events: DbTaskEventRow[] }, + ) => void, + ): () => void; } export type TaskStoreEmitOptions = { taskId: string; - runId: string; body: JsonObject; - type: TaskEventType; }; export type TaskStoreGetEventsOptions = { @@ -82,14 +89,22 @@ export type TaskStoreGetEventsOptions = { after?: number | undefined; }; export interface TaskStore { - get(taskId: string): Promise; createTask(task: TaskSpec): Promise<{ taskId: string }>; claimTask(): Promise; - heartbeat(runId: string): Promise; - listStaleTasks(): Promise<{ tasks: DbTaskRow }>; - setStatus(runId: string, status: Status): Promise; - emit({ taskId, runId, body, type }: TaskStoreEmitOptions): Promise; - getEvents({ + completeTask(options: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise; + heartbeatTask(taskId: string): Promise; + listStaleTasks(options: { + timeoutS: number; + }): Promise<{ + tasks: { taskId: string }[]; + }>; + + emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + listEvents({ taskId, after, }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }>; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 6eb112fa3c..8da6f02507 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -82,7 +82,7 @@ export async function createRouter( const databaseTaskStore = await DatabaseTaskStore.create( await database.getClient(), ); - const taskBroker = new StorageTaskBroker(databaseTaskStore); + const taskBroker = new StorageTaskBroker(databaseTaskStore, logger); const actionRegistry = new TemplateActionRegistry(); const worker = new TaskWorker({ logger, @@ -124,7 +124,7 @@ export async function createRouter( error: job.error, }); }) - // curl -X POST -d '{"templateName":"springboot-template","values": {"storePath":"https://github.com/jhaals/foo", "component_id":"woop", "description": "apa", "owner": "me" }}' -H 'Content-Type: application/json' localhost:7000/api/scaffolder/v2/tasks + .post('/v2/tasks', async (req, res) => { const templateName: string = req.body.templateName; const values: TemplaterValues = { From 9b0bba5439c7fe7383f897756b6394d807a62393 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 2 Feb 2021 16:25:45 +0100 Subject: [PATCH 035/158] Reshuffle api endpoints --- .../scaffolder-backend/src/service/router.ts | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 8da6f02507..16cefa5297 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -124,68 +124,6 @@ export async function createRouter( error: job.error, }); }) - - .post('/v2/tasks', async (req, res) => { - const templateName: string = req.body.templateName; - const values: TemplaterValues = { - ...req.body.values, - destination: { - git: parseGitUrl(req.body.values.storePath), - }, - }; - const template = await entityClient.findTemplate(templateName); - - const validationResult: ValidatorResult = validate( - values, - template.spec.schema, - ); - - if (!validationResult.valid) { - res.status(400).json({ errors: validationResult.errors }); - return; - } - const taskSpec = templateEntityToSpec(template, values); - const result = await taskBroker.dispatch(taskSpec); - - res.status(201).json({ id: result.taskId }); - }) - - .get('/v2/tasks/:taskId/eventstream', async (req, res) => { - const { taskId } = req.params; - const after = Number(req.query.after) || undefined; - logger.info('event stream opened'); - - // Mandatory headers and http status to keep connection open - res.writeHead(200, { - Connection: 'keep-alive', - 'Cache-Control': 'no-cache', - 'Content-Type': 'text/event-stream', - }); - // After client opens connection send all nests as string - const unsubscribe = taskBroker.observe( - { taskId, after }, - (error, { events }) => { - logger.error( - `Received error from event stream when observing task ${taskId}`, - error, - ); - for (const event of events) { - res.write(`event:${JSON.stringify(event)}\n\n`); - if (event.type === 'completion') { - unsubscribe(); - res.end(); - } - } - }, - ); - // When client closes connection we update the clients list - // avoiding the disconnected one - req.on('close', () => { - unsubscribe(); - logger.info('event stream closed'); - }); - }) - .post('/v1/jobs', async (req, res) => { const templateName: string = req.body.templateName; const values: TemplaterValues = { @@ -282,6 +220,68 @@ export async function createRouter( res.status(201).json({ id: job.id }); }); + // NOTE: The v2 API is unstable + router + .post('/v2/tasks', async (req, res) => { + const templateName: string = req.body.templateName; + const values: TemplaterValues = { + ...req.body.values, + destination: { + git: parseGitUrl(req.body.values.storePath), + }, + }; + const template = await entityClient.findTemplate(templateName); + + const validationResult: ValidatorResult = validate( + values, + template.spec.schema, + ); + + if (!validationResult.valid) { + res.status(400).json({ errors: validationResult.errors }); + return; + } + const taskSpec = templateEntityToSpec(template, values); + const result = await taskBroker.dispatch(taskSpec); + + res.status(201).json({ id: result.taskId }); + }) + .get('/v2/tasks/:taskId/eventstream', async (req, res) => { + const { taskId } = req.params; + const after = Number(req.query.after) || undefined; + logger.info('event stream opened'); + + // Mandatory headers and http status to keep connection open + res.writeHead(200, { + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/event-stream', + }); + // After client opens connection send all nests as string + const unsubscribe = taskBroker.observe( + { taskId, after }, + (error, { events }) => { + logger.error( + `Received error from event stream when observing task ${taskId}`, + error, + ); + for (const event of events) { + res.write(`event:${JSON.stringify(event)}\n\n`); + if (event.type === 'completion') { + unsubscribe(); + res.end(); + } + } + }, + ); + // When client closes connection we update the clients list + // avoiding the disconnected one + req.on('close', () => { + unsubscribe(); + logger.info('event stream closed'); + }); + }); + const app = express(); app.set('logger', logger); app.use('/', router); From 615103a631993ec21745e6b3d93652f74c48cd4d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 2 Feb 2021 16:33:00 +0100 Subject: [PATCH 036/158] Add changesets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Patrik Oldsberg --- .changeset/afraid-teachers-cross.md | 29 +++++++++++++++++++++++++++++ .changeset/large-terms-invite.md | 7 +++++++ 2 files changed, 36 insertions(+) create mode 100644 .changeset/afraid-teachers-cross.md create mode 100644 .changeset/large-terms-invite.md diff --git a/.changeset/afraid-teachers-cross.md b/.changeset/afraid-teachers-cross.md new file mode 100644 index 0000000000..8fc376441f --- /dev/null +++ b/.changeset/afraid-teachers-cross.md @@ -0,0 +1,29 @@ +--- +'@backstage/create-app': patch +--- + +Pass on plugin database management instance that is now required by the scaffolder plugin. + +To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`: + +```diff +export default async function createPlugin({ + logger, + config, ++ database, +}: PluginEnvironment) { + +// ...omitted... + + return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + dockerClient, + entityClient, ++ database, + }); +} +``` diff --git a/.changeset/large-terms-invite.md b/.changeset/large-terms-invite.md new file mode 100644 index 0000000000..6653ac8e1e --- /dev/null +++ b/.changeset/large-terms-invite.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend. + +This API should be considered unstable until used by the scaffolder frontend. From a173d47527b0ceb960c3a2a815b59edf2ebb6e2e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 2 Feb 2021 16:45:28 +0100 Subject: [PATCH 037/158] Delete knexfile --- plugins/scaffolder-backend/knexfile.js | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 plugins/scaffolder-backend/knexfile.js diff --git a/plugins/scaffolder-backend/knexfile.js b/plugins/scaffolder-backend/knexfile.js deleted file mode 100644 index f469df4c08..0000000000 --- a/plugins/scaffolder-backend/knexfile.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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. - */ - -module.exports = { - development: { - client: 'sqlite3', - connection: ':memory:', - migrations: { - directory: 'migrations', - }, - }, -}; From c386de896d65dbe05db2a1c23d0bc2a68a4aa864 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 2 Feb 2021 16:46:11 +0100 Subject: [PATCH 038/158] Remove unused dependencies --- plugins/scaffolder-backend/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index bdecf6196e..9f45c218dc 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -55,11 +55,9 @@ "jsonschema": "^1.2.6", "knex": "^0.21.6", "morgan": "^1.10.0", - "p-queue": "^6.3.0", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0", - "luxon": "^1.25.0" + "yaml": "^1.10.0" }, "devDependencies": { "@backstage/cli": "^0.5.0", From 0c00a0c14c979c61a59ba002ead4f90fceacf852 Mon Sep 17 00:00:00 2001 From: Debajyoti Halder Date: Tue, 2 Feb 2021 23:07:42 +0530 Subject: [PATCH 039/158] cost-insights: Add better type names from patch --- .../AlertInsights/AlertInsights.tsx | 20 ++++++------- .../src/components/BarChart/BarChartLabel.tsx | 4 +-- .../src/components/BarChart/BarChartSteps.tsx | 4 +-- .../ProductInsights/ProductInsights.tsx | 28 +++++++------------ 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx index 5204686118..7360c99cf7 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx @@ -124,18 +124,18 @@ export const AlertInsights = ({ setSnackbarOpen(!!error); }, [error]); - function onSnooze(alertProp: Alert) { - setAlert(alertProp); + function onSnooze(alertToSnooze: Alert) { + setAlert(alertToSnooze); setStatus(AlertStatus.Snoozed); } - function onAccept(alertProp: Alert) { - setAlert(alertProp); + function onAccept(alertToAccept: Alert) { + setAlert(alertToAccept); setStatus(AlertStatus.Accepted); } - function onDismiss(alertProp: Alert) { - setAlert(alertProp); + function onDismiss(alertToDismiss: Alert) { + setAlert(alertToDismiss); setStatus(AlertStatus.Dismissed); } @@ -148,8 +148,8 @@ export const AlertInsights = ({ setStatus(null); } - function onDialogFormSubmit(inputData: any) { - setData(inputData); + function onDialogFormSubmit(formData: any) { + setData(formData); } function onSummaryButtonClick() { @@ -175,10 +175,10 @@ export const AlertInsights = ({
{isAlertInsightSectionDisplayed && ( - {active.map((alertItem, index) => ( + {active.map((activeAlert, index) => ( ) => { +}: PropsWithChildren) => { const classes = useBarChartLabelStyles(); const translateX = width * -0.5; diff --git a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx index ec573da68f..8157e05444 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { ButtonBase } from '@material-ui/core'; import { useBarChartStepperStyles as useStyles } from '../../utils/styles'; -export type BarChartStepsObject = { +export type BarChartStepsProps = { steps: number; activeStep: number; onClick: (index: number) => void; @@ -28,7 +28,7 @@ export const BarChartSteps = ({ steps, activeStep, onClick, -}: BarChartStepsObject) => { +}: BarChartStepsProps) => { const classes = useStyles(); const handleOnClick = (index: number) => ( event: React.MouseEvent, diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx index b13376d24a..5702e67172 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx @@ -82,32 +82,24 @@ export const ProductInsights = ({ ); useEffect(() => { - async function getAllProductInsights( - groupId: string, - projectId: Maybe, - productIds: Product[], - lastCompleteBillingDateString: string, - ) { + async function getAllProductInsights() { try { dispatchLoadingProducts(true); const responses = await Promise.allSettled( - productIds.map(product => + products.map(product => client.getProductInsights({ - group: groupId, - project: projectId, + group: group, + project: project, product: product.kind, - intervals: intervalsOf( - DEFAULT_DURATION, - lastCompleteBillingDateString, - ), + intervals: intervalsOf(DEFAULT_DURATION, lastCompleteBillingDate), }), ), ).then(settledResponseOf); - const initialStatesNow = initialStatesOf(productIds, responses).sort( + const updatedInitialStates = initialStatesOf(products, responses).sort( totalAggregationSort, ); - setStates(initialStatesNow); + setStates(updatedInitialStates); } catch (e) { setError(e); } finally { @@ -115,7 +107,7 @@ export const ProductInsights = ({ } } - getAllProductInsights(group, project, products, lastCompleteBillingDate); + getAllProductInsights(); }, [ client, group, @@ -128,8 +120,8 @@ export const ProductInsights = ({ useEffect( function handleOnLoaded() { if (onceRef.current) { - const currentProducts = initialStates.map(state => state.product); - onLoaded(currentProducts); + const initialProducts = initialStates.map(state => state.product); + onLoaded(initialProducts); } else { onceRef.current = true; } From 6b26c9f41eb059968db05998d5f4bbf50d75fbb4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 21:57:41 +0100 Subject: [PATCH 040/158] circleci: migrate to new composability API --- .changeset/curly-crabs-compete.md | 5 +++++ plugins/circleci/dev/index.tsx | 4 ++-- plugins/circleci/src/components/Router.tsx | 19 +++++++++++++++---- plugins/circleci/src/index.ts | 12 ++++++++++-- plugins/circleci/src/plugin.test.ts | 4 ++-- plugins/circleci/src/plugin.ts | 11 ++++++++++- 6 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 .changeset/curly-crabs-compete.md diff --git a/.changeset/curly-crabs-compete.md b/.changeset/curly-crabs-compete.md new file mode 100644 index 0000000000..552a41a9f1 --- /dev/null +++ b/.changeset/curly-crabs-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-circleci': patch +--- + +Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`. diff --git a/plugins/circleci/dev/index.tsx b/plugins/circleci/dev/index.tsx index 812a5585d4..19a32c0d04 100644 --- a/plugins/circleci/dev/index.tsx +++ b/plugins/circleci/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { circleCIPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(circleCIPlugin).render(); diff --git a/plugins/circleci/src/components/Router.tsx b/plugins/circleci/src/components/Router.tsx index 5fe9a20eff..b3a0d2c3e2 100644 --- a/plugins/circleci/src/components/Router.tsx +++ b/plugins/circleci/src/components/Router.tsx @@ -21,15 +21,25 @@ import { BuildWithStepsPage } from './BuildWithStepsPage/'; import { BuildsPage } from './BuildsPage'; import { CIRCLECI_ANNOTATION } from '../constants'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { MissingAnnotationEmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[CIRCLECI_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + return ; + } + + return ( } /> /> ); +}; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index c6cb140208..1e8b213279 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -14,8 +14,16 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + circleCIPlugin, + circleCIPlugin as plugin, + EntityCircleCIContent, +} from './plugin'; export * from './api'; export * from './route-refs'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isPluginApplicableToEntity, + isPluginApplicableToEntity as isCircleCIAvailable, +} from './components/Router'; export { CIRCLECI_ANNOTATION } from './constants'; diff --git a/plugins/circleci/src/plugin.test.ts b/plugins/circleci/src/plugin.test.ts index 821a503257..bdd637027c 100644 --- a/plugins/circleci/src/plugin.test.ts +++ b/plugins/circleci/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { circleCIPlugin } from './plugin'; describe('circleci', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(circleCIPlugin).toBeDefined(); }); }); diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index c5bb122ed3..4e0d485726 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -18,10 +18,12 @@ import { createPlugin, createApiFactory, discoveryApiRef, + createRoutableExtension, } from '@backstage/core'; import { circleCIApiRef, CircleCIApi } from './api'; +import { circleCIRouteRef } from './route-refs'; -export const plugin = createPlugin({ +export const circleCIPlugin = createPlugin({ id: 'circleci', apis: [ createApiFactory({ @@ -31,3 +33,10 @@ export const plugin = createPlugin({ }), ], }); + +export const EntityCircleCIContent = circleCIPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: circleCIRouteRef, + }), +); From 302795d107c2a2dedd521bf644b2d85283311f76 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 22:16:45 +0100 Subject: [PATCH 041/158] cloudbuild: migrate to now composability API --- .changeset/four-rings-push.md | 5 +++ plugins/cloudbuild/dev/index.tsx | 4 +-- plugins/cloudbuild/package.json | 1 + .../cloudbuild/src/components/Cards/Cards.tsx | 24 ++++++++------ plugins/cloudbuild/src/components/Router.tsx | 20 +++++++++--- plugins/cloudbuild/src/index.ts | 14 ++++++-- plugins/cloudbuild/src/plugin.test.ts | 4 +-- plugins/cloudbuild/src/plugin.ts | 32 ++++++++++++++++++- 8 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 .changeset/four-rings-push.md diff --git a/.changeset/four-rings-push.md b/.changeset/four-rings-push.md new file mode 100644 index 0000000000..500dd0e812 --- /dev/null +++ b/.changeset/four-rings-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cloudbuild': patch +--- + +Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`. diff --git a/plugins/cloudbuild/dev/index.tsx b/plugins/cloudbuild/dev/index.tsx index 264d6f801f..26fb65d151 100644 --- a/plugins/cloudbuild/dev/index.tsx +++ b/plugins/cloudbuild/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { cloudbuildPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(cloudbuildPlugin).render(); diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 3e5bbef3a9..7e9e13816b 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/core": "^0.5.0", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx index bbfb0b59c3..75ae33027e 100644 --- a/plugins/cloudbuild/src/components/Cards/Cards.tsx +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -17,6 +17,7 @@ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; import { @@ -72,12 +73,13 @@ const WidgetContent = ({ }; export const LatestWorkflowRunCard = ({ - entity, branch = 'master', }: { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch: string; }) => { + const { entity } = useEntity(); const errorApi = useApi(errorApiRef); const projectId = entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] || ''; @@ -104,13 +106,17 @@ export const LatestWorkflowRunCard = ({ }; export const LatestWorkflowsForBranchCard = ({ - entity, branch = 'master', }: { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch: string; -}) => ( - - - -); +}) => { + const { entity } = useEntity(); + + return ( + + + + ); +}; diff --git a/plugins/cloudbuild/src/components/Router.tsx b/plugins/cloudbuild/src/components/Router.tsx index af4959f4be..c31947a531 100644 --- a/plugins/cloudbuild/src/components/Router.tsx +++ b/plugins/cloudbuild/src/components/Router.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Routes, Route } from 'react-router'; import { rootRouteRef, buildRouteRef } from '../plugin'; import { WorkflowRunDetails } from './WorkflowRunDetails'; @@ -25,11 +26,19 @@ import { MissingAnnotationEmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[CLOUDBUILD_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => - // TODO(shmidt-i): move warning to a separate standardized component - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + // TODO(shmidt-i): move warning to a separate standardized component + return ; + } + return ( ) ); +}; diff --git a/plugins/cloudbuild/src/index.ts b/plugins/cloudbuild/src/index.ts index 616bb6b4e7..a846792fa2 100644 --- a/plugins/cloudbuild/src/index.ts +++ b/plugins/cloudbuild/src/index.ts @@ -13,8 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin } from './plugin'; +export { + cloudbuildPlugin, + cloudbuildPlugin as plugin, + EntityCloudbuildContent, + EntityLatestCloudbuildRunCard, + EntityLatestCloudbuildsForBranchCard, +} from './plugin'; export * from './api'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isPluginApplicableToEntity, + isPluginApplicableToEntity as isCloudbuildAvailable, +} from './components/Router'; export * from './components/Cards'; export { CLOUDBUILD_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/cloudbuild/src/plugin.test.ts b/plugins/cloudbuild/src/plugin.test.ts index dae16f1d1b..ea042a901a 100644 --- a/plugins/cloudbuild/src/plugin.test.ts +++ b/plugins/cloudbuild/src/plugin.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { plugin } from './plugin'; +import { cloudbuildPlugin } from './plugin'; describe('cloudbuild', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(cloudbuildPlugin).toBeDefined(); }); }); diff --git a/plugins/cloudbuild/src/plugin.ts b/plugins/cloudbuild/src/plugin.ts index ee997cc7ec..f5aaf4696a 100644 --- a/plugins/cloudbuild/src/plugin.ts +++ b/plugins/cloudbuild/src/plugin.ts @@ -18,6 +18,8 @@ import { createRouteRef, createApiFactory, googleAuthApiRef, + createRoutableExtension, + createComponentExtension, } from '@backstage/core'; import { cloudbuildApiRef, CloudbuildClient } from './api'; @@ -31,7 +33,7 @@ export const buildRouteRef = createRouteRef({ title: 'Cloudbuild Run', }); -export const plugin = createPlugin({ +export const cloudbuildPlugin = createPlugin({ id: 'cloudbuild', apis: [ createApiFactory({ @@ -42,4 +44,32 @@ export const plugin = createPlugin({ }, }), ], + routes: { + entityContent: rootRouteRef, + }, }); + +export const EntityCloudbuildContent = cloudbuildPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); + +export const EntityLatestCloudbuildRunCard = cloudbuildPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowRunCard), + }, + }), +); + +export const EntityLatestCloudbuildsForBranchCard = cloudbuildPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowsForBranchCard), + }, + }), +); From f54025dde83bb62a251e5b5afa4c2a94a21f8c74 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 22:18:45 +0100 Subject: [PATCH 042/158] lighthouse: grab entity from context --- plugins/lighthouse/src/Router.tsx | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx index 0634a0aaa0..643d30cdc2 100644 --- a/plugins/lighthouse/src/Router.tsx +++ b/plugins/lighthouse/src/Router.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; +import { useEntity } from '@backstage/plugin-catalog-react'; import AuditList from './components/AuditList'; import AuditView, { AuditViewContent } from './components/AuditView'; import CreateAudit, { CreateAuditContent } from './components/CreateAudit'; @@ -35,15 +36,27 @@ export const Router = () => ( ); -export const EmbeddedRouter = ({ entity }: { entity: Entity }) => - !isLighthouseAvailable(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const EmbeddedRouter = (_props: Props) => { + const { entity } = useEntity(); + + if (!isLighthouseAvailable(entity)) { + return ( + + ); + } + + return ( } /> } /> } /> ); +}; From b7fb8984c08a133b786c8fea46e26a172827f85b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 22:24:42 +0100 Subject: [PATCH 043/158] org: update cards to grab entity from context --- .../Group/GroupProfile/GroupProfileCard.tsx | 42 ++++++++++--------- .../Group/MembersList/MembersListCard.tsx | 9 ++-- .../Cards/OwnershipCard/OwnershipCard.tsx | 11 +++-- .../User/UserProfileCard/UserProfileCard.tsx | 7 ++-- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index f5a1acd5e6..3b8d93b121 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,7 +21,7 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-react'; +import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; @@ -33,26 +33,29 @@ import { generatePath, Link as RouterLink } from 'react-router-dom'; const GroupLink = ({ groupName, index = 0, - entity, }: { groupName: string; index?: number; - entity: Entity; -}) => ( - <> - {index >= 1 ? ', ' : ''} - - [{groupName}] - - -); + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}) => { + const { entity } = useEntity(); + return ( + <> + {index >= 1 ? ', ' : ''} + + [{groupName}] + + + ); +}; const CardTitle = ({ title }: { title: string }) => ( @@ -61,12 +64,13 @@ const CardTitle = ({ title }: { title: string }) => ( ); export const GroupProfileCard = ({ - entity: group, variant, }: { - entity: GroupEntity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: GroupEntity; variant: string; }) => { + const group = useEntity().entity as GroupEntity; const { metadata: { name, description }, spec: { profile }, diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index f644e7db7e..7f8ede2eb7 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -21,6 +21,7 @@ import { } from '@backstage/catalog-model'; import { Avatar, InfoCard, Progress, useApi } from '@backstage/core'; import { + useEntity, catalogApiRef, entityRouteParams, } from '@backstage/plugin-catalog-react'; @@ -105,11 +106,11 @@ const MemberComponent = ({ ); }; -export const MembersListCard = ({ - entity: groupEntity, -}: { - entity: GroupEntity; +export const MembersListCard = (_props: { + /** @deprecated The entity is now grabbed from context instead */ + entity?: GroupEntity; }) => { + const groupEntity = useEntity().entity as GroupEntity; const { metadata: { name: groupName }, spec: { profile }, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 60f06bd53a..80e80bddcc 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -16,7 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { InfoCard, Progress, useApi } from '@backstage/core'; -import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + isOwnerOf, + useEntity, +} from '@backstage/plugin-catalog-react'; import { pageTheme } from '@backstage/theme'; import { Box, @@ -113,12 +117,13 @@ const EntityCountTile = ({ }; export const OwnershipCard = ({ - entity, variant, }: { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; variant: string; }) => { + const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); const { loading, diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index bdd0cd15f6..1cf7c968b3 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,7 +19,7 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-react'; +import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; @@ -60,12 +60,13 @@ const CardTitle = ({ title }: { title?: string }) => ) : null; export const UserProfileCard = ({ - entity: user, variant, }: { - entity: UserEntity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: UserEntity; variant: string; }) => { + const user = useEntity().entity as UserEntity; const { metadata: { name: metaName }, spec: { profile }, From f5f45744e36e09b9ac21442292780bf9a6f59826 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 23:11:22 +0100 Subject: [PATCH 044/158] github-actions: migrate to new composability API --- .changeset/selfish-years-pump.md | 5 +++ plugins/github-actions/dev/index.tsx | 4 +- plugins/github-actions/package.json | 1 + .../src/components/Cards/Cards.tsx | 21 ++++++---- .../Cards/RecentWorkflowRunsCard.tsx | 6 ++- .../github-actions/src/components/Router.tsx | 20 +++++++-- plugins/github-actions/src/index.ts | 15 ++++++- plugins/github-actions/src/plugin.test.ts | 4 +- plugins/github-actions/src/plugin.ts | 41 ++++++++++++++++++- 9 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 .changeset/selfish-years-pump.md diff --git a/.changeset/selfish-years-pump.md b/.changeset/selfish-years-pump.md new file mode 100644 index 0000000000..48994a1d9e --- /dev/null +++ b/.changeset/selfish-years-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': patch +--- + +Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGitHubActionsContent`, entity conditional as `isGitHubActionsAvailable`, and entity cards as `EntityLatestGitHubActionRunCard`, `EntityLatestGitHubActionsForBranchCard`, and `EntityRecentGitHubActionsRunsCard`. diff --git a/plugins/github-actions/dev/index.tsx b/plugins/github-actions/dev/index.tsx index 812a5585d4..9156224202 100644 --- a/plugins/github-actions/dev/index.tsx +++ b/plugins/github-actions/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { githubActionsPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(githubActionsPlugin).render(); diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index beb35b3cb0..33378c2022 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/core": "^0.5.0", "@backstage/integration": "^0.3.1", "@backstage/theme": "^0.2.2", diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index f1c27cb666..202bf5aa92 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -17,6 +17,7 @@ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { @@ -81,11 +82,11 @@ const WidgetContent = ({ }; export const LatestWorkflowRunCard = ({ - entity, branch = 'master', // Display the card full height suitable for variant, }: Props) => { + const { entity } = useEntity(); const config = useApi(configApiRef); const errorApi = useApi(errorApiRef); // TODO: Get github hostname from metadata annotation @@ -121,17 +122,21 @@ export const LatestWorkflowRunCard = ({ }; type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch: string; variant?: string; }; export const LatestWorkflowsForBranchCard = ({ - entity, branch = 'master', variant, -}: Props) => ( - - - -); +}: Props) => { + const { entity } = useEntity(); + + return ( + + + + ); +}; diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index aa30423c3e..4425bc8364 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -23,6 +23,7 @@ import { useApi, } from '@backstage/core'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Button, Link } from '@material-ui/core'; import React, { useEffect } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; @@ -33,7 +34,8 @@ import { WorkflowRunStatus } from '../WorkflowRunStatus'; const firstLine = (message: string): string => message.split('\n')[0]; export type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch?: string; dense?: boolean; limit?: number; @@ -41,12 +43,12 @@ export type Props = { }; export const RecentWorkflowRunsCard = ({ - entity, branch, dense = false, limit = 5, variant, }: Props) => { + const { entity } = useEntity(); const config = useApi(configApiRef); const errorApi = useApi(errorApiRef); // TODO: Get github hostname from metadata annotation diff --git a/plugins/github-actions/src/components/Router.tsx b/plugins/github-actions/src/components/Router.tsx index e7430d40ed..e867b05b18 100644 --- a/plugins/github-actions/src/components/Router.tsx +++ b/plugins/github-actions/src/components/Router.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Routes, Route } from 'react-router'; import { rootRouteRef, buildRouteRef } from '../plugin'; import { WorkflowRunDetails } from './WorkflowRunDetails'; @@ -25,10 +26,20 @@ import { MissingAnnotationEmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + return ( + + ); + } + return ( ) ); +}; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 24fe6fc90d..6dd3b9724e 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -14,8 +14,19 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + githubActionsPlugin, + githubActionsPlugin as plugin, + EntityGitHubActionsContent, + EntityLatestGitHubActionRunCard, + EntityLatestGitHubActionsForBranchCard, + EntityRecentGitHubActionsRunsCard, +} from './plugin'; export * from './api'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isPluginApplicableToEntity, + isPluginApplicableToEntity as isGitHubActionsAvailable, +} from './components/Router'; export * from './components/Cards'; export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/github-actions/src/plugin.test.ts b/plugins/github-actions/src/plugin.test.ts index fa1075cbc8..53bc96c77a 100644 --- a/plugins/github-actions/src/plugin.test.ts +++ b/plugins/github-actions/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { githubActionsPlugin } from './plugin'; describe('github-actions', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(githubActionsPlugin).toBeDefined(); }); }); diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index 39ffc468b1..71c05745d4 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -20,6 +20,8 @@ import { createRouteRef, createApiFactory, githubAuthApiRef, + createRoutableExtension, + createComponentExtension, } from '@backstage/core'; import { githubActionsApiRef, GithubActionsClient } from './api'; @@ -34,7 +36,7 @@ export const buildRouteRef = createRouteRef({ title: 'GitHub Actions Workflow Run', }); -export const plugin = createPlugin({ +export const githubActionsPlugin = createPlugin({ id: 'github-actions', apis: [ createApiFactory({ @@ -44,4 +46,41 @@ export const plugin = createPlugin({ new GithubActionsClient({ configApi, githubAuthApi }), }), ], + routes: { + entityContent: rootRouteRef, + }, }); + +export const EntityGitHubActionsContent = githubActionsPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); + +export const EntityLatestGitHubActionRunCard = githubActionsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowRunCard), + }, + }), +); + +export const EntityLatestGitHubActionsForBranchCard = githubActionsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowsForBranchCard), + }, + }), +); + +export const EntityRecentGitHubActionsRunsCard = githubActionsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.RecentWorkflowRunsCard), + }, + }), +); From 025c0c7bf8b8ecfaf368243a8e21c52ed3bfe643 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 23:30:10 +0100 Subject: [PATCH 045/158] jenkins: migrate to new composability API --- .changeset/popular-cars-eat.md | 5 +++++ plugins/jenkins/dev/index.tsx | 4 ++-- plugins/jenkins/src/components/Router.tsx | 18 ++++++++++++++---- plugins/jenkins/src/index.ts | 13 +++++++++++-- plugins/jenkins/src/plugin.test.ts | 4 ++-- plugins/jenkins/src/plugin.ts | 21 ++++++++++++++++++++- 6 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 .changeset/popular-cars-eat.md diff --git a/.changeset/popular-cars-eat.md b/.changeset/popular-cars-eat.md new file mode 100644 index 0000000000..6d3bcdfab2 --- /dev/null +++ b/.changeset/popular-cars-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`. diff --git a/plugins/jenkins/dev/index.tsx b/plugins/jenkins/dev/index.tsx index 812a5585d4..8beb6eabc1 100644 --- a/plugins/jenkins/dev/index.tsx +++ b/plugins/jenkins/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { jenkinsPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(jenkinsPlugin).render(); diff --git a/plugins/jenkins/src/components/Router.tsx b/plugins/jenkins/src/components/Router.tsx index 91bcde2a8b..e310cd4193 100644 --- a/plugins/jenkins/src/components/Router.tsx +++ b/plugins/jenkins/src/components/Router.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { Route, Routes } from 'react-router'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { buildRouteRef, rootRouteRef } from '../plugin'; import { DetailedViewPage } from './BuildWithStepsPage/'; import { JENKINS_ANNOTATION } from '../constants'; @@ -25,10 +26,19 @@ import { CITable } from './BuildsPage/lib/CITable'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[JENKINS_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => { - return !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + return ; + } + + return ( } /> } /> diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts index 30fa0c70a5..6d0e7837d3 100644 --- a/plugins/jenkins/src/index.ts +++ b/plugins/jenkins/src/index.ts @@ -14,8 +14,17 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + jenkinsPlugin, + jenkinsPlugin as plugin, + EntityJenkinsContent, + EntityLatestJenkinsRunCard, +} from './plugin'; export { LatestRunCard } from './components/Cards'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isPluginApplicableToEntity, + isPluginApplicableToEntity as isJenkinsAvailable, +} from './components/Router'; export { JENKINS_ANNOTATION } from './constants'; export * from './api'; diff --git a/plugins/jenkins/src/plugin.test.ts b/plugins/jenkins/src/plugin.test.ts index 8c3f057ecb..0498b80b90 100644 --- a/plugins/jenkins/src/plugin.test.ts +++ b/plugins/jenkins/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { jenkinsPlugin } from './plugin'; describe('jenkins', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(jenkinsPlugin).toBeDefined(); }); }); diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts index 23c54743a3..85001d9bbc 100644 --- a/plugins/jenkins/src/plugin.ts +++ b/plugins/jenkins/src/plugin.ts @@ -19,6 +19,8 @@ import { createRouteRef, createApiFactory, discoveryApiRef, + createRoutableExtension, + createComponentExtension, } from '@backstage/core'; import { jenkinsApiRef, JenkinsApi } from './api'; @@ -32,7 +34,7 @@ export const buildRouteRef = createRouteRef({ title: 'Jenkins run', }); -export const plugin = createPlugin({ +export const jenkinsPlugin = createPlugin({ id: 'jenkins', apis: [ createApiFactory({ @@ -41,4 +43,21 @@ export const plugin = createPlugin({ factory: ({ discoveryApi }) => new JenkinsApi({ discoveryApi }), }), ], + routes: { + entityContent: rootRouteRef, + }, }); + +export const EntityJenkinsContent = jenkinsPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); +export const EntityLatestJenkinsRunCard = jenkinsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/Cards').then(m => m.LatestRunCard), + }, + }), +); From 7716d1d709d3258a86ca3f9cd1ca2d5536e7f805 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 23:47:25 +0100 Subject: [PATCH 046/158] kafka: migrate to new composability API --- .changeset/sixty-lemons-agree.md | 5 +++++ plugins/kafka/dev/index.tsx | 4 ++-- plugins/kafka/src/Router.tsx | 23 ++++++++++++++++++----- plugins/kafka/src/index.ts | 12 ++++++++++-- plugins/kafka/src/plugin.test.ts | 4 ++-- plugins/kafka/src/plugin.ts | 13 ++++++++++++- 6 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 .changeset/sixty-lemons-agree.md diff --git a/.changeset/sixty-lemons-agree.md b/.changeset/sixty-lemons-agree.md new file mode 100644 index 0000000000..7fc438b433 --- /dev/null +++ b/.changeset/sixty-lemons-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka': patch +--- + +Migrate to new composability API, exporting the plugin instance as `kafkaPlugin`, entity content as `EntityKafkaContent`, and entity conditional as `isKafkaAvailable`. diff --git a/plugins/kafka/dev/index.tsx b/plugins/kafka/dev/index.tsx index 264d6f801f..5506d47026 100644 --- a/plugins/kafka/dev/index.tsx +++ b/plugins/kafka/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { kafkaPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(kafkaPlugin).render(); diff --git a/plugins/kafka/src/Router.tsx b/plugins/kafka/src/Router.tsx index b445f8f408..13503fefe8 100644 --- a/plugins/kafka/src/Router.tsx +++ b/plugins/kafka/src/Router.tsx @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import React from 'react'; import { Route, Routes } from 'react-router'; - +import { useEntity } from '@backstage/plugin-catalog-react'; import { rootCatalogKafkaRouteRef } from './plugin'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from './constants'; import { KafkaTopicsForConsumer } from './components/ConsumerGroupOffsets/ConsumerGroupOffsets'; @@ -26,10 +26,23 @@ import { MissingAnnotationEmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => { - return !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + return ( + + ); + } + + return ( { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(kafkaPlugin).toBeDefined(); }); }); diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index 878a7f56e9..832099a30c 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -16,6 +16,7 @@ import { createApiFactory, createPlugin, + createRoutableExtension, createRouteRef, discoveryApiRef, } from '@backstage/core'; @@ -27,7 +28,7 @@ export const rootCatalogKafkaRouteRef = createRouteRef({ title: 'Kafka', }); -export const plugin = createPlugin({ +export const kafkaPlugin = createPlugin({ id: 'kafka', apis: [ createApiFactory({ @@ -36,4 +37,14 @@ export const plugin = createPlugin({ factory: ({ discoveryApi }) => new KafkaBackendClient({ discoveryApi }), }), ], + routes: { + entityContent: rootCatalogKafkaRouteRef, + }, }); + +export const EntityKafkaContent = kafkaPlugin.provide( + createRoutableExtension({ + component: () => import('./Router').then(m => m.Router), + mountPoint: rootCatalogKafkaRouteRef, + }), +); From 64b9efac2e12a8523451e98f8ce12055beb61ef5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 23:51:30 +0100 Subject: [PATCH 047/158] kubernets: migrate to new composability API --- .changeset/shy-maps-guess.md | 5 +++++ plugins/kubernetes/dev/index.tsx | 4 ++-- plugins/kubernetes/package.json | 1 + plugins/kubernetes/src/Router.tsx | 11 +++++++++-- plugins/kubernetes/src/index.ts | 6 +++++- plugins/kubernetes/src/plugin.test.ts | 4 ++-- plugins/kubernetes/src/plugin.ts | 13 ++++++++++++- 7 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 .changeset/shy-maps-guess.md diff --git a/.changeset/shy-maps-guess.md b/.changeset/shy-maps-guess.md new file mode 100644 index 0000000000..2a50d3d461 --- /dev/null +++ b/.changeset/shy-maps-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Migrate to new composability API, exporting the plugin instance as `kubernetesPlugin` and entity content as `EntityKubernetesContent`. diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index f352fb9c34..de93d21348 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src'; +import { kubernetesPlugin } from '../src'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(kubernetesPlugin).render(); diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 1d985c0d10..3dc53a9ea4 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", "@backstage/plugin-kubernetes-backend": "^0.2.6", diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 20a4c04285..36851b3c46 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -16,15 +16,22 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Route, Routes } from 'react-router-dom'; - import { rootCatalogKubernetesRouteRef } from './plugin'; import { KubernetesContent } from './components/KubernetesContent'; import { MissingAnnotationEmptyState } from '@backstage/core'; const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; -export const Router = ({ entity }: { entity: Entity }) => { +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + const kubernetesAnnotationValue = entity.metadata.annotations?.[KUBERNETES_ANNOTATION]; diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index 8b5969666c..eca6b068f1 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -13,5 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin } from './plugin'; +export { + kubernetesPlugin, + kubernetesPlugin as plugin, + EntityKubernetesContent, +} from './plugin'; export { Router } from './Router'; diff --git a/plugins/kubernetes/src/plugin.test.ts b/plugins/kubernetes/src/plugin.test.ts index 20c50a813a..f176b7f451 100644 --- a/plugins/kubernetes/src/plugin.test.ts +++ b/plugins/kubernetes/src/plugin.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { plugin } from './plugin'; +import { kubernetesPlugin } from './plugin'; describe('kubernetes', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(kubernetesPlugin).toBeDefined(); }); }); diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index cff9e1468b..af29df3dcc 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -19,6 +19,7 @@ import { createRouteRef, discoveryApiRef, googleAuthApiRef, + createRoutableExtension, } from '@backstage/core'; import { KubernetesBackendClient } from './api/KubernetesBackendClient'; import { kubernetesApiRef } from './api/types'; @@ -30,7 +31,7 @@ export const rootCatalogKubernetesRouteRef = createRouteRef({ title: 'Kubernetes', }); -export const plugin = createPlugin({ +export const kubernetesPlugin = createPlugin({ id: 'kubernetes', apis: [ createApiFactory({ @@ -47,4 +48,14 @@ export const plugin = createPlugin({ }, }), ], + routes: { + entityContent: rootCatalogKubernetesRouteRef, + }, }); + +export const EntityKubernetesContent = kubernetesPlugin.provide( + createRoutableExtension({ + component: () => import('./Router').then(m => m.Router), + mountPoint: rootCatalogKubernetesRouteRef, + }), +); From c5ab91ce3c697da8453123e64ad574c692356fe1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Feb 2021 23:57:50 +0100 Subject: [PATCH 048/158] newrelic: migrate to new composability API --- .changeset/shiny-falcons-marry.md | 5 +++++ plugins/newrelic/dev/index.tsx | 4 ++-- plugins/newrelic/src/index.ts | 6 +++++- plugins/newrelic/src/plugin.test.ts | 4 ++-- plugins/newrelic/src/plugin.ts | 14 +++++++++++++- 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 .changeset/shiny-falcons-marry.md diff --git a/.changeset/shiny-falcons-marry.md b/.changeset/shiny-falcons-marry.md new file mode 100644 index 0000000000..4dda68d693 --- /dev/null +++ b/.changeset/shiny-falcons-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic': patch +--- + +Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`. diff --git a/plugins/newrelic/dev/index.tsx b/plugins/newrelic/dev/index.tsx index 812a5585d4..9ca421f94a 100644 --- a/plugins/newrelic/dev/index.tsx +++ b/plugins/newrelic/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { newRelicPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(newRelicPlugin).render(); diff --git a/plugins/newrelic/src/index.ts b/plugins/newrelic/src/index.ts index 3a0a0fe2d3..aa4e990d6c 100644 --- a/plugins/newrelic/src/index.ts +++ b/plugins/newrelic/src/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + newRelicPlugin, + newRelicPlugin as plugin, + NewRelicPage, +} from './plugin'; diff --git a/plugins/newrelic/src/plugin.test.ts b/plugins/newrelic/src/plugin.test.ts index 17429a95e0..f2e8fde924 100644 --- a/plugins/newrelic/src/plugin.test.ts +++ b/plugins/newrelic/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { newRelicPlugin } from './plugin'; describe('newrelic', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(newRelicPlugin).toBeDefined(); }); }); diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index 5f5ba88617..d89aa813e9 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -19,6 +19,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + createRoutableExtension, } from '@backstage/core'; import { NewRelicClient, newRelicApiRef } from './api'; import NewRelicComponent from './components/NewRelicComponent'; @@ -28,7 +29,7 @@ export const rootRouteRef = createRouteRef({ title: 'newrelic', }); -export const plugin = createPlugin({ +export const newRelicPlugin = createPlugin({ id: 'newrelic', apis: [ createApiFactory({ @@ -40,4 +41,15 @@ export const plugin = createPlugin({ register({ router }) { router.addRoute(rootRouteRef, NewRelicComponent); }, + routes: { + root: rootRouteRef, + }, }); + +export const NewRelicPage = newRelicPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/NewRelicComponent').then(m => m.default), + mountPoint: rootRouteRef, + }), +); From f20f0465141f676f42b39ed467f9eaac5439fe2f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Feb 2021 00:03:07 +0100 Subject: [PATCH 049/158] github-actions: fix card tests --- .../src/components/Cards/RecentWorkflowRunsCard.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 0deb4ae39d..18cc59439b 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -22,6 +22,7 @@ import { ConfigApi, ConfigReader, } from '@backstage/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { render } from '@testing-library/react'; @@ -84,7 +85,9 @@ describe('', () => { configApi, )} > - + + + , From 5479e4eef0d82efbc348a901d17a6c50f6a09aa3 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Feb 2021 23:27:20 -0500 Subject: [PATCH 050/158] Improve display of errors --- .../components/ApiExplorerTable/ApiExplorerTable.tsx | 10 ++++------ .../src/components/CatalogTable/CatalogTable.tsx | 12 +++++++----- .../lighthouse/src/components/AuditList/index.tsx | 6 +++--- .../techdocs/src/reader/components/TechDocsHome.tsx | 8 +++++++- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 101c28332a..743366bdcd 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -27,6 +27,7 @@ import { TableFilter, TableState, useQueryParamState, + WarningPanel, } from '@backstage/core'; import { EntityRefLink, @@ -35,7 +36,6 @@ import { getEntityRelations, } from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; import React from 'react'; import { ApiTypeTitle } from '../ApiDefinitionCard'; @@ -151,11 +151,9 @@ export const ApiExplorerTable = ({ if (error) { return ( -
- - Error encountered while fetching catalog entities. {error.toString()} - -
+ + {error.toString()} + ); } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 30a19145f2..97c7ccfa18 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -19,7 +19,7 @@ import { RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; -import { Table, TableColumn, TableProps } from '@backstage/core'; +import { Table, TableColumn, TableProps, WarningPanel } from '@backstage/core'; import { EntityRefLink, EntityRefLinks, @@ -29,7 +29,6 @@ import { import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; -import { Alert } from '@material-ui/lab'; import React from 'react'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntities'; @@ -128,9 +127,12 @@ export const CatalogTable = ({ if (error) { return (
- - Error encountered while fetching catalog entities. {error.toString()} - + + {error.toString()} +
); } diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index ce0fcdb46e..8d7f769909 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -17,7 +17,6 @@ import React, { useState, useMemo, ReactNode } from 'react'; import { useLocalStorage, useAsync } from 'react-use'; import { useNavigate } from 'react-router-dom'; import { Grid, Button } from '@material-ui/core'; -import Alert from '@material-ui/lab/Alert'; import Pagination from '@material-ui/lab/Pagination'; import { InfoCard, @@ -28,6 +27,7 @@ import { HeaderLabel, Progress, useApi, + WarningPanel, } from '@backstage/core'; import { lighthouseApiRef } from '../../api'; @@ -85,9 +85,9 @@ const AuditList = () => { content = ; } else if (error) { content = ( - + {error.message} - + ); } diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 2d8b5c7dcc..f2317f0b43 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -21,6 +21,7 @@ import { Page, Progress, useApi, + WarningPanel, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; @@ -62,7 +63,12 @@ export const TechDocsHome = () => { subtitle="Documentation available in Backstage" /> -

{error.message}

+ + {error.message} +
); From f5e564cd67239fdbc187f1e05402ea9df66f02f8 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Feb 2021 23:28:06 -0500 Subject: [PATCH 051/158] Add changeset --- .changeset/hot-pants-taste.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/hot-pants-taste.md diff --git a/.changeset/hot-pants-taste.md b/.changeset/hot-pants-taste.md new file mode 100644 index 0000000000..5cce26c5bf --- /dev/null +++ b/.changeset/hot-pants-taste.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-techdocs': patch +--- + +Improve display of error messages From 2b9d16b39772d9d11972916a92ee8a2255346ee7 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Feb 2021 23:32:06 -0500 Subject: [PATCH 052/158] Add period for consistency in error msg --- plugins/techdocs/src/reader/components/TechDocsHome.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index f2317f0b43..d5d3af67cb 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -65,7 +65,7 @@ export const TechDocsHome = () => { {error.message} From 81a039cc9fcaefdc9723815f1a4e41dfd76df10e Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Feb 2021 23:49:15 -0500 Subject: [PATCH 053/158] Fix test --- .../catalog/src/components/CatalogTable/CatalogTable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 46d6010f2f..16c91c0d6c 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -50,7 +50,7 @@ describe('CatalogTable component', () => { ), ); const errorMessage = await rendered.findByText( - /Error encountered while fetching catalog entities./, + /Could not fetch catalog entities./, ); expect(errorMessage).toBeInTheDocument(); }); From c0ddb3256339ffad3657fb509a9e3b8c1646fdaa Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Wed, 3 Feb 2021 00:02:50 -0500 Subject: [PATCH 054/158] Fix tests --- .../src/components/ApiExplorerTable/ApiExplorerTable.test.tsx | 2 +- plugins/lighthouse/src/components/AuditList/index.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx index 494695e277..bcb310be46 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx @@ -61,7 +61,7 @@ describe('ApiCatalogTable component', () => { ), ); const errorMessage = await rendered.findByText( - /Error encountered while fetching catalog entities./, + /Could not fetch catalog entities./, ); expect(errorMessage).toBeInTheDocument(); }); diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 5fdc4781ce..4657066249 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -162,7 +162,7 @@ describe('AuditList', () => { , ), ); - const element = await rendered.findByTestId('error-message'); + const element = await rendered.findByText(/Could not load audit list./); expect(element).toBeInTheDocument(); }); }); From d8386f780a6c60371c5e213584e14683482930f2 Mon Sep 17 00:00:00 2001 From: Manoj - Date: Wed, 3 Feb 2021 16:54:44 +1100 Subject: [PATCH 055/158] added condition handling the case - bitbucket.org --- packages/integration/src/bitbucket/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index f22c727f63..4359762676 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -38,7 +38,7 @@ export async function getBitbucketDefaultBranch( var response = await fetch(branchUrl, getBitbucketRequestOptions(config)); - if (response.status === 404) { + if (response.status === 404 && !isHosted) { // First try the new format, and then if it gets specifically a 404 it should try the old format // (to support old Atlassian Bitbucket v5.11.1 format ) branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; From 2a3d7599e60870e292e949177773da40cd2bb1e9 Mon Sep 17 00:00:00 2001 From: Manoj - Date: Wed, 3 Feb 2021 18:59:25 +1100 Subject: [PATCH 056/158] added test cases to include both the endpoints --- .../integration/src/bitbucket/core.test.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 1287ab4d66..5bb06bcfa3 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -111,6 +111,7 @@ describe('bitbucket core', () => { describe('getBitbucketDownloadUrl', () => { it('add path param if a path is specified for Bitbucket Server', async () => { + const defaultBranchResponse = { displayId: 'main', }; @@ -125,6 +126,7 @@ describe('bitbucket core', () => { ), ), ); + const config: BitbucketIntegrationConfig = { host: 'bitbucket.mycompany.net', apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', @@ -249,6 +251,42 @@ describe('bitbucket core', () => { config, ); expect(defaultBranch).toEqual('main'); - }); + }); + + it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => { + const defaultBranchResponse = { + displayId: 'main', + }; + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', + (_, res, ctx) => + res( + ctx.status(404), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + ); + const config: BitbucketIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const defaultBranch = await getBitbucketDefaultBranch( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', + config, + ); + expect(defaultBranch).toEqual('main'); + }); + }); }); From 2e62aea6fbc190ca9f791030809d0891aadc5a0b Mon Sep 17 00:00:00 2001 From: Kaparwan Manoj Date: Wed, 3 Feb 2021 19:25:43 +1100 Subject: [PATCH 057/158] changeset created --- .changeset/wicked-boxes-taste.md | 5 ++ package.json | 2 +- yarn.lock | 96 +++++++++++++++++++++++++------- 3 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 .changeset/wicked-boxes-taste.md diff --git a/.changeset/wicked-boxes-taste.md b/.changeset/wicked-boxes-taste.md new file mode 100644 index 0000000000..a875d1c50d --- /dev/null +++ b/.changeset/wicked-boxes-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': major +--- + +#4322 bitbucket own hosted v5.11.1 branchUrl fix and enabled error tracing… #4347 diff --git a/package.json b/package.json index 758a8ba884..fd130fcbbd 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "version": "1.0.0", "devDependencies": { - "@changesets/cli": "^2.11.0", + "@changesets/cli": "^2.14.0", "@octokit/openapi-types": "^2.2.0", "@spotify/eslint-config-oss": "^1.0.1", "@spotify/prettier-config": "^9.0.0", diff --git a/yarn.lock b/yarn.lock index 2ac7dffa6a..3626741854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2655,21 +2655,22 @@ resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-5.0.0.tgz#3ba791f37b90e7f6170d252b63aacfcae943c039" integrity sha512-WmKrB/575EJCzbeSJR3YQ5sET5FaizeljLRw1382qVUeGqzuWBgIS+AF5a0FO51uQTrDpoRgvuHC2IWVsgwkkA== -"@changesets/apply-release-plan@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.0.0.tgz#e78efb56a4e459a8dab814ba43045f2ace0f27c9" - integrity sha512-MrcUd8wIlQ4S/PznzqJVsmnEpUGfPEkCGF54iqt8G05GEqi/zuxpoTfebcScpj5zeiDyxFIcA9RbeZ3pvJJxoA== +"@changesets/apply-release-plan@^4.2.0": + version "4.2.0" + resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.2.0.tgz#f1005815f27c3238f66507e90c6ae14d8fc62b41" + integrity sha512-/vt6UwgQldhOw93Gb8llI5OuYGlJt2+U45AfcXsoxzl8gZzCmChGm3vUaQJYbmtL8TbL8OOVXHRIKJJidMNPKw== dependencies: - "@babel/runtime" "^7.4.4" - "@changesets/config" "^1.2.0" + "@babel/runtime" "^7.10.4" + "@changesets/config" "^1.5.0" "@changesets/get-version-range-type" "^0.3.2" "@changesets/git" "^1.0.5" - "@changesets/types" "^3.1.0" + "@changesets/types" "^3.3.0" "@manypkg/get-packages" "^1.0.1" + detect-indent "^6.0.0" fs-extra "^7.0.1" lodash.startcase "^4.4.0" outdent "^0.5.0" - prettier "^1.18.2" + prettier "^1.19.1" resolve-from "^5.0.0" semver "^5.4.1" @@ -2685,23 +2686,35 @@ "@manypkg/get-packages" "^1.0.1" semver "^5.4.1" -"@changesets/cli@^2.11.0": - version "2.12.0" - resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.12.0.tgz#26124b051e6ce6dcc5aa4595588c8cb2ce3e4363" - integrity sha512-dGdFkg75zsaEObsG8gwMLglS6sJVjXWwgVTAzEIjqIoWVnKwqZqccTb4gn0noq47uCwy7SqxiAJqGibIy9UOKw== +"@changesets/assemble-release-plan@^4.1.0": + version "4.1.0" + resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-4.1.0.tgz#091e5e768dfe51835937e71d1ebaca1c9d6de55b" + integrity sha512-dMqe2L5Pn4UGTW89kOuuCuZD3pQFZj1Sxv92ZW4S98sXGsxcb2PdW+PeHbQ7tawkCYCOvzhXxAlN4OdF2DlDKQ== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/apply-release-plan" "^4.0.0" - "@changesets/assemble-release-plan" "^4.0.0" - "@changesets/config" "^1.4.0" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.1.3" + "@changesets/get-dependents-graph" "^1.2.0" + "@changesets/types" "^3.3.0" + "@manypkg/get-packages" "^1.0.1" + semver "^5.4.1" + +"@changesets/cli@^2.14.0": + version "2.14.0" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.14.0.tgz#b8d1d33d832c640ce0b95333bbd8d5ac5b9c9824" + integrity sha512-rbQMRDXl1cXOglnjUvYyrFLlYBbS0YZdfxZfW3ZbGLzLoS4n50+B9fLSE9oW20hQuL3zAnyLyacb9cwNhF2lig== + dependencies: + "@babel/runtime" "^7.10.4" + "@changesets/apply-release-plan" "^4.2.0" + "@changesets/assemble-release-plan" "^4.1.0" + "@changesets/config" "^1.5.0" + "@changesets/errors" "^0.1.4" + "@changesets/get-dependents-graph" "^1.2.0" "@changesets/get-release-plan" "^2.0.1" - "@changesets/git" "^1.0.6" + "@changesets/git" "^1.1.0" "@changesets/logger" "^0.0.5" "@changesets/pre" "^1.0.4" "@changesets/read" "^0.4.6" - "@changesets/types" "^3.2.0" + "@changesets/types" "^3.3.0" "@changesets/write" "^0.1.3" "@manypkg/get-packages" "^1.0.1" "@types/semver" "^6.0.0" @@ -2721,7 +2734,7 @@ term-size "^2.1.0" tty-table "^2.8.10" -"@changesets/config@^1.2.0", "@changesets/config@^1.4.0": +"@changesets/config@^1.2.0": version "1.4.0" resolved "https://registry.npmjs.org/@changesets/config/-/config-1.4.0.tgz#c157a4121f198b749f2bbc2e9015b6e976ece7d6" integrity sha512-eoTOcJ6py7jBDY8rUXwEGxR5UtvUX+p//0NhkVpPGcnvIeITHq+DOIsuWyGzWcb+1FaYkof3CCr32/komZTu4Q== @@ -2734,6 +2747,19 @@ fs-extra "^7.0.1" micromatch "^4.0.2" +"@changesets/config@^1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@changesets/config/-/config-1.5.0.tgz#6d58b01e45916318d3f39e9cde86a5d69dc58ed8" + integrity sha512-Bl9nLVYcwFCpd9jpzcOsExZk1NuTYX20D2YWHCdS1xll3W0yOdSUlWLUCCfugN1l3+yTR6iDW6q9o6vpCevWvA== + dependencies: + "@changesets/errors" "^0.1.4" + "@changesets/get-dependents-graph" "^1.2.0" + "@changesets/logger" "^0.0.5" + "@changesets/types" "^3.3.0" + "@manypkg/get-packages" "^1.0.1" + fs-extra "^7.0.1" + micromatch "^4.0.2" + "@changesets/errors@^0.1.4": version "0.1.4" resolved "https://registry.npmjs.org/@changesets/errors/-/errors-0.1.4.tgz#f79851746c43679a66b383fdff4c012f480f480d" @@ -2752,6 +2778,17 @@ fs-extra "^7.0.1" semver "^5.4.1" +"@changesets/get-dependents-graph@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.2.0.tgz#6986927a5fec60bc6fcc76f966efae2ac30c05ed" + integrity sha512-96NInEKpEZH8KvmXyh42PynXVAdq3kQ9VjAeswHtJ3umUCeTF42b/KVXaov+5P1RNnaUVtRuEwzs4syGuowDTw== + dependencies: + "@changesets/types" "^3.3.0" + "@manypkg/get-packages" "^1.0.1" + chalk "^2.1.0" + fs-extra "^7.0.1" + semver "^5.4.1" + "@changesets/get-release-plan@^2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-2.0.1.tgz#b95d8f1a3cc719ff4b42b9b9aae72458d8787c13" @@ -2770,7 +2807,7 @@ resolved "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz#8131a99035edd11aa7a44c341cbb05e668618c67" integrity sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg== -"@changesets/git@^1.0.5", "@changesets/git@^1.0.6": +"@changesets/git@^1.0.5": version "1.0.6" resolved "https://registry.npmjs.org/@changesets/git/-/git-1.0.6.tgz#057e627e5d3fcb74bf6c18d7284e130ba5a7632e" integrity sha512-e0M06XuME3W5lGhz+CO0vLc60u+hLk/pYjOx/6GXEWuQrwtGgeycFIfRgRt8qTs664o1oKtVHBbd7ItpoWgFfA== @@ -2782,6 +2819,18 @@ is-subdir "^1.1.1" spawndamnit "^2.0.0" +"@changesets/git@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@changesets/git/-/git-1.1.0.tgz#ecaa8058ac87b450c09da0e74b68e6854cd0742c" + integrity sha512-f/2rQynT+JiAL/V0V/GQdXhLkcb86ELg3UwH3fQO4wVdfUbE9NHIHq9ohJdH1Ymh0Lv48F/b38aWZ5v2sKiF3w== + dependencies: + "@babel/runtime" "^7.10.4" + "@changesets/errors" "^0.1.4" + "@changesets/types" "^3.1.1" + "@manypkg/get-packages" "^1.0.1" + is-subdir "^1.1.1" + spawndamnit "^2.0.0" + "@changesets/logger@^0.0.5": version "0.0.5" resolved "https://registry.npmjs.org/@changesets/logger/-/logger-0.0.5.tgz#68305dd5a643e336be16a2369cb17cdd8ed37d4c" @@ -2827,6 +2876,11 @@ resolved "https://registry.npmjs.org/@changesets/types/-/types-3.2.0.tgz#d8306d7219c3b19b6d860ddeb9d7374e2dd6b035" integrity sha512-rAmPtOyXpisEEE25CchKNUAf2ApyAeuZ/h78YDoqKZaCk5tUD0lgYZGPIRV9WTPoqNjJULIym37ogc6pkax5jg== +"@changesets/types@^3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@changesets/types/-/types-3.3.0.tgz#04cd8184b2d2da760667bd89bf9b930938dbd96e" + integrity sha512-rJamRo+OD/MQekImfIk07JZwYSB18iU6fYL8xOg0gfAiTh1a1+OlR1fPIxm55I7RsWw812is2YcPPwXdIewrhA== + "@changesets/write@^0.1.3": version "0.1.3" resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.3.tgz#00ae575af50274773d7493e77fb96838a08ad8ad" @@ -21292,7 +21346,7 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^1.18.2: +prettier@^1.18.2, prettier@^1.19.1: version "1.19.1" resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== From 5fa3bdb5537fa20f4c6948715eee11b923fc19e6 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 19 Jan 2021 17:04:06 +0100 Subject: [PATCH 058/158] Add support for links in ItemCard and fix height of cards with tags vs cards without tags --- .changeset/four-shrimps-flash.md | 7 ++++ .../src/layout/ItemCard/ItemCard.stories.tsx | 35 +++++++++++++++++-- .../core/src/layout/ItemCard/ItemCard.tsx | 26 +++++++++++--- .../src/reader/components/TechDocsHome.tsx | 17 ++++----- 4 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 .changeset/four-shrimps-flash.md diff --git a/.changeset/four-shrimps-flash.md b/.changeset/four-shrimps-flash.md new file mode 100644 index 0000000000..fb155bafd7 --- /dev/null +++ b/.changeset/four-shrimps-flash.md @@ -0,0 +1,7 @@ +--- +'@backstage/core': patch +'@backstage/plugin-techdocs': patch +--- + +Add `href` in addition to `onClick` to `ItemCard`. Ensure that the height of a +`ItemCard` with and without tags is equal. diff --git a/packages/core/src/layout/ItemCard/ItemCard.stories.tsx b/packages/core/src/layout/ItemCard/ItemCard.stories.tsx index 75031a0765..6e534f6f5c 100644 --- a/packages/core/src/layout/ItemCard/ItemCard.stories.tsx +++ b/packages/core/src/layout/ItemCard/ItemCard.stories.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { ItemCard } from '.'; import { Grid } from '@material-ui/core'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { ItemCard } from '.'; export default { title: 'Layout/Item Card', @@ -63,5 +64,35 @@ export const Tags = () => ( label="Button" />
+ + +
); + +export const Link = () => ( + + + + + + + + + + +); diff --git a/packages/core/src/layout/ItemCard/ItemCard.tsx b/packages/core/src/layout/ItemCard/ItemCard.tsx index 991a05708f..3b0727b64f 100644 --- a/packages/core/src/layout/ItemCard/ItemCard.tsx +++ b/packages/core/src/layout/ItemCard/ItemCard.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Button, Card, Chip, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; -import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core'; +import { Link } from '../../components'; const useStyles = makeStyles(theme => ({ header: { @@ -30,6 +31,9 @@ const useStyles = makeStyles(theme => ({ overflow: 'hidden', textOverflow: 'ellipsis', }, + withTags: { + height: 'calc(175px - 32px - 8px)', + }, footer: { display: 'flex', flexDirection: 'row-reverse', @@ -43,7 +47,9 @@ type ItemCardProps = { type?: string; label: string; onClick?: () => void; + href?: string; }; + export const ItemCard = ({ description, tags, @@ -51,6 +57,7 @@ export const ItemCard = ({ type, label, onClick, + href, }: ItemCardProps) => { const classes = useStyles(); @@ -64,13 +71,22 @@ export const ItemCard = ({ {tags?.map((tag, i) => ( ))} - + 0 ? classes.withTags : '' + }`} + > {description}
- + {!href && ( + + )} + {href && {label}}
diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 2d8b5c7dcc..f064c35517 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -25,13 +25,12 @@ import { import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; import React from 'react'; -import { generatePath, useNavigate } from 'react-router-dom'; +import { generatePath } from 'react-router-dom'; import { useAsync } from 'react-use'; import { rootDocsRouteRef } from '../../plugin'; export const TechDocsHome = () => { const catalogApi = useApi(catalogApiRef); - const navigate = useNavigate(); const { value, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities(); @@ -80,15 +79,11 @@ export const TechDocsHome = () => { ? value.map((entity, index: number) => ( - navigate( - generatePath(rootDocsRouteRef.path, { - namespace: entity.metadata.namespace ?? 'default', - kind: entity.kind, - name: entity.metadata.name, - }), - ) - } + href={generatePath(rootDocsRouteRef.path, { + namespace: entity.metadata.namespace ?? 'default', + kind: entity.kind, + name: entity.metadata.name, + })} title={entity.metadata.name} label="Read Docs" description={entity.metadata.description} From 9cdd8f888896937878d26df01a6912c2f58ab265 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 19 Jan 2021 18:22:38 +0100 Subject: [PATCH 059/158] Add ecosystem explorer --- packages/app/src/components/Root/Root.tsx | 2 + packages/app/src/plugins.ts | 1 + plugins/ecosystem/.eslintrc.js | 3 + plugins/ecosystem/README.md | 24 ++++++ plugins/ecosystem/dev/index.tsx | 58 +++++++++++++ plugins/ecosystem/docs/domains.png | Bin 0 -> 791113 bytes plugins/ecosystem/package.json | 50 +++++++++++ .../components/DomainCard/DomainCard.test.tsx | 49 +++++++++++ .../src/components/DomainCard/DomainCard.tsx | 38 +++++++++ .../src/components/DomainCard/index.ts | 16 ++++ .../DomainExplorer/DomainExplorer.tsx | 33 ++++++++ .../src/components/DomainExplorer/index.ts | 16 ++++ .../DomainExplorerPage/DomainExplorerPage.tsx | 78 ++++++++++++++++++ .../components/DomainExplorerPage/index.ts | 16 ++++ plugins/ecosystem/src/index.ts | 16 ++++ plugins/ecosystem/src/plugin.test.ts | 22 +++++ plugins/ecosystem/src/plugin.ts | 29 +++++++ plugins/ecosystem/src/setupTests.ts | 17 ++++ 18 files changed, 468 insertions(+) create mode 100644 plugins/ecosystem/.eslintrc.js create mode 100644 plugins/ecosystem/README.md create mode 100644 plugins/ecosystem/dev/index.tsx create mode 100644 plugins/ecosystem/docs/domains.png create mode 100644 plugins/ecosystem/package.json create mode 100644 plugins/ecosystem/src/components/DomainCard/DomainCard.test.tsx create mode 100644 plugins/ecosystem/src/components/DomainCard/DomainCard.tsx create mode 100644 plugins/ecosystem/src/components/DomainCard/index.ts create mode 100644 plugins/ecosystem/src/components/DomainExplorer/DomainExplorer.tsx create mode 100644 plugins/ecosystem/src/components/DomainExplorer/index.ts create mode 100644 plugins/ecosystem/src/components/DomainExplorerPage/DomainExplorerPage.tsx create mode 100644 plugins/ecosystem/src/components/DomainExplorerPage/index.ts create mode 100644 plugins/ecosystem/src/index.ts create mode 100644 plugins/ecosystem/src/plugin.test.ts create mode 100644 plugins/ecosystem/src/plugin.ts create mode 100644 plugins/ecosystem/src/setupTests.ts diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 52dd397418..29211dc42a 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -20,6 +20,7 @@ import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; import RuleIcon from '@material-ui/icons/AssignmentTurnedIn'; import MapIcon from '@material-ui/icons/MyLocation'; +import LayersIcon from '@material-ui/icons/Layers'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import MoneyIcon from '@material-ui/icons/MonetizationOn'; @@ -86,6 +87,7 @@ const Root = ({ children }: PropsWithChildren<{}>) => ( {/* End global nav */} + +``` diff --git a/plugins/ecosystem/dev/index.tsx b/plugins/ecosystem/dev/index.tsx new file mode 100644 index 0000000000..30a071862b --- /dev/null +++ b/plugins/ecosystem/dev/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Entity } from '@backstage/catalog-model'; +import { createDevApp } from '@backstage/dev-utils'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { plugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(plugin) + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + async getEntities() { + const domainNames = [ + 'playback', + 'artists', + 'payments', + 'analytics', + 'songs', + 'devops', + ]; + + return { + items: domainNames.map( + (n, i) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: n, + description: `Everything about ${n}`, + tags: i % 2 === 0 ? [n] : undefined, + }, + spec: { + owner: `${n}@example.com`, + }, + } as Entity), + ), + }; + }, + } as CatalogApi), + }) + .render(); diff --git a/plugins/ecosystem/docs/domains.png b/plugins/ecosystem/docs/domains.png new file mode 100644 index 0000000000000000000000000000000000000000..70f3be02d63719dc22a83a4e81b823dc85025eac GIT binary patch literal 791113 zcmZU42|QHa|F=?P7iDKGNkn5w7|SG;5L5JxY?CcnNA`6ll&LIZr)&vHw(Pr6WZ%iY zk9}usvp=Kn@4r0HGq2Zmk9)Rz&bjBD_vih2pZAXpv{@MW7-?u|SacrTHKd_o1kupY zKfORleNyulLrZ-y@1UjiNJmRc_>nv8g@dyl4b6l12}$%%j5_#Uugn}ovp=|V&F~H^ z#PjR}2KCn=p6m(H=QwoVNnRUE_(x?U=GGb8P1%{AUgAGG|LjuyiuKR1 z&blVC&28141}xcQxz~qG=z}hIr()S?7G;a>#xmrei55@P6AKOexX1ARp*i?94a=i5 zXI{Gd$up>miZTn!D^mWMnY~wVvBpkedt;VDG0xmSFE2#%h~t-JATN+<{+4b1f=;$O z%_HQ+hCPlN+fj1I@t38VND@kxgS)%Cvr}kA)55-4`)?$!R`hJmzZqNJT{W*fF7ldcms)IWx%4`wejawcUB`{@$ zCRBLjV!7Xq4{6x#bxxg}rMz}X;mK*ZyPY2)-VyO=f=NC!-v60FynCU5O6ri4SbRWo zFU$L6Yj(O28qSv24~6Lso9-#|oX`q$ODCYXF-bBV2_FXExL@V3d3E-xc6V7?hvyHE zAN0?OWCNl1RWY2$9M-9yM(lPpqxJ(9doMvY40X@Eyg_q&<7auVtprVcAkC)_?Tce) zD}!}^Fb#*0=Gc|be)`GaMrTfA+{EHWuXaxTGob!l^PD=*Tj2}zF(Q1~EWEeBMk_AT zln4W_(J+RJW}dsos~g%9eb$t!4YS|9&g!c2VDnx|n z$O?{x_RKw#WqQ$^>2|Z8#vqit+0RX*p5X@4X)e@_Gmx(F>}+trT+;3Z#?Mb4F~^H^ z(*3@J%l!5D{kJFamnt~^c_*dy=QH1Ama)2=OM9ZwXRwh$ZhkmUKNT9dln+u6mNB89c9&Pv;7<|w^L#w zP3sip^_$Ly^T*nl-B{$9;Mjd<{z|4vq~Onaw+UsZREZk`ZNEESov&t`(wuUb(l-Zh zi&>tBz4mFMU*O!cCYlEFl4;17$XCfK@c7U7WyPyGe=!8!xnZSX#g>`imevp95&zEF z^FF=hr8VdkOG)b3Z8b>X-N5`WU#znizRE-HWtW=fm@ob4yq92^0J$R7@nKP0S5)#r z$^*g!muy0Im96}E#HNGTay+M~pa@nhn$WF~s=zo-JBqE}^a?n1JCr_*T&Euf=CM6`^+dsu zCIQluJ7U;pNO*belUxs~HLEadD>E(kaHz!71B6UB0Db{{@fk!ae&aLXE8){q{C?BG z@j&(wvjQD5remyI`B!zi2ZwVu}hd9r>l#G38U8ocviM9J^Rnxp5cG-E!S-kvvX{{@ZS)ECE zfsct#PfNFFie&mTg|f*o+vduSlDw+53cIne@uCWs${@uq+ouZ!3(o}08pl;8?IyNL z0$xHY*h-*IvyPP3pPj)gkCxmfV>WqKt2;}6?^VQA#5FnO4yoIJ!V0V$_HOk;EzB*l zZ?N15ExKCNQS_&1S{igcq$0*^fCaO>QQ5imW>Uh3+oy(b@ffV;tR}C<>dV}HD?=u^ z!QML~H%4OAqskt|J#>A1L%UcArUfhVi99v}*Z>;)EHhS|^}Im@VM0Q2TJi94@(w-N z2YngtLcT^3rre?Q(m39Jc-yWi?sisFc@w^AUU=oRouF}3tw2ybPg0hsf!4W0z9=DXY8Cu6#Z&-s{aBT@Khb?^{pcfJ1 zni87v{ju-#BJ`(Hl;y$C$(1;gPOr{+9Utywph@lA;GML$ADJLGzDO_J0DX$kiPF=E zCcfqGEu4?ab~F5L6y^u7->Odv1i&Q`kLDz=T~vb{K9tcBe1Ls8`RUfR1Fl2{F)3w!Jg*bm)K9IFERe+4#r?#C%vPLTr0f#85*sHgUry%8=ayPbzMs528PM6>X(shh zs`5!--leBAdHaQq1tWzsk4RSZh_;?I4OM>cwJoWML!9;i>e!op%k{flzGVLIk>z2H zNzS@ljKm1UE5PfBYl~D1sV}xiERsFEioCq&J=vU#kQI>etSn~i2r;M+!sETl=edWV-%$fmmv3q6*XTF*{8~;=e3Mv&5owz=Q#VDC z90?nZoM95*4IERa0zddlQHICX>W9Q!Vk3sBSrM%OqFNe#mQdLq=)JwcNXB8{{=aSpH~6 zuDUU`Jk{uKznxZ8C+D-{CE;~Eg_-5A&hVoghNjYIu^EGZsYwME993_=-;Vnw3Pi*ZOWH4uFaHx`HH)4QdxbN=yZ4R(b z`3AS!TTcQ9lB+=i;3FwQ2%(+~DCqs=*<*zZf(^7+`w~r~q$MTJXbrD%)7> zfH>3*+KD!WyO6l>TLUH|W65u>FBz1dy=L2#mq$}Sjyz~%py_v|xt$6ghe9sg#9d7m z-p%kWrO#mJrg5x0^Ujj)_Lw4#lvGS2*B(T3=KRG=cX!S_*VbM-NoqF~6ucS~Qy(+3 zxHmRt#zf=xn8BbafbiD!}rfz zqoG1$>hrTSG)M=UbN@+WK)s*-qN&$uo`3JOv0*gy)c-iBm+$AZ|CO2%^qKa*!t_t6 zaWtAnS~@z^yOE8%ot>-4OPJ?`ya|f>fWhs-GY=XXF7earjE>=zU26Gb4o^%xP4pjv zY+x=otZZS{b~k)o+)m4(QS${+gD!TSR>Hn6&aNIHUv<&HDL~Zl>0_X%@ZTh!PU@m2 z`j3RQVD5IpiZ^cFkQLQn6c!d%bGLl~GQ4~LKiR4Osf)h!^mGFOfj&MyH+&Rsz})SD zaw;k+Kv{X9yu1uGg^Y)vtEZK(jH`#(zlHo)Id|zwc@1>+nA{xqAGkS=0ssPh)^`H)MhTT{bnV+UZl!BL`nQXOp`QF4R7w)}bMP zOHNtsZ-)Ob>VK;IH|swhcJ5j*7ivyVjsNNO|78B(k^gVTzjZ$QpE~7~mHw|f|5wz1 zvZ?`3+y1}$;@^(``;^+#8jNbd{~k3B#&<0ccIrS1INXIiq28&V+3AlqpZe$8zjtc* zg5uhHgkmKP&21W;yP8jY&uliVr7u^<&Lky#tT_ItqR_~(6Dv-LwH?QlfFF!3Umsc9 z{@&SH@KEdlSyT2xtT0!j*=NKb2wmcZIZc{#n)Qoylr0JYF*7qgJ>5`)C_IZt-JTHM zx)?BeX{I5-=lJNz+vmzJfOpLCmm3*UamD9nW@l#`8XIT+++BZfx#-ba*r+x*dlNDo z>AR@c>f`#t--D8N7V?{a>o@%S!$zK&I^6D`9;D(xo(DF+PRNpnj63i=ZbR+(0E^MF zqz{dt`+|$)K>PrSusp(28dmz8 znTNLZwVWH7IgoU+wv8biPm`|_Qo~5_;ypKYwLo9rO(HQ4>$P{>w*#(iFdbK%L?aT* zAWur!4)E(oIp!8dKU&G_8!3}*@Rg%7PFW!<(wcNsC(xj0XSh6{Q!2S9xs@3tiLcxjaNU_(p_RmD^&q=O1y5|$#GK+i z8T7C$@Z=PsL@i?mp{3*9#1QeV%+1GJd|NI%v(Z&_MSCY*>R9p~5xc$8rB|8PjPp>` zR|m&VfBXDgmk_e~8&O%Ag5r zEv$A%C#s*c*PqfF5z#HZD*H%8VtII}Jq%t`*h9!x2?#o3j zUAdF!Viqrgq1`@1XQuYzC*N50(xi#A<#b@6=iB$<(SZsFQ1_W`>& zO(2(K7RIc740Lvhrg^pzvEgcs`eA=7BG|+FHWcdEv6>gW(vtM&H&69dx@!2)*C!tX zWnw^S{GDHwkV5C}dTf94$kIiev6&c)p1&`%QeP?yOncD-@!D#JaqO z*Wh%dVuepRqFm52N_`Z%t}P;h0HWd3>J4h#g1*d$zn7b38;)kREsVeUjA<-5T7RP? z?NN>Y6+ST<7;>BLcl-R0+ zz^f0ok{knX`>$N;g>SNF`=qC>-9$D!wVO-FR1KCu;1-QfLH?SszP*f2$br6n{m=fj z@i>FJxwfuQAs`z*26b1t^WN1gt_NvzV${jdM%xPY(e37cg19n<;1=M@X%I?7EM^;p zSH0Z_O?MSF4rZL|48T1cQim47_w`)3jEc8qCrPB{eP9*$pqC$z4NY^r|47I$>;|Ro zvqn{{23~I#4rEh`vlKefOwURm@QT_$n4L}$T!P-b5-~fthv;3Jol4=FsZo-VKA+aB ze1%ezZ3KKYiaOzC-OBjQh85nbTRd=`zaYZbg)~OX{_&5|r#!=kLWeB)ylT^Lpmf`A zJWwxz5CxC~+7+Z)YRbal*JFWknr^|@q|CM*pIJD0lNJ0yjY)Y*+Unhp1Na;-_!V6@ zxEmZo*!viljF85+g-#md!UIw;&CQ{?`p_*CK=jYSTw^Va_7+k_n_(vm7zxCEE>zx1UTuYpTSCJu13bEcrBFF4x&IK zTcPn?#{4hrne`qrI;liyE?<@Cu~Sr>9S~puJ6ib$3yh@TU7!|?1J-T&udDB6}UakmZC~(|n?G-CH@1-E`JLA~_rryp z(Qy7XINTj~6~1LR&S~P%bf0zr5~K#(S5*u$p9Qh-!y^r%0o8wE?(f z#5=Z&0j;`&7MpYbn`FXEv1FOFR$jZ>$9~oT7 zGAfXM44zA>s_m>2XSCDq(*3%RR|X%t-u6=W-k42b_z)4?u?sm7i#qa;Fb8)`&WDjd zBAC@OLvc>GcA7B;q}B6ACvhui>#KaXHjNOS{xjw^WPgcW59U@b48y03Fz=DJ&^-oour+Ro7(pF0H>)9U8pDX)WI8{ z4i}Xf7fl5qt19$ zMlVviMcRyEVhYbKt#8Q08k6X#f>$zW<$BG#S5dV?+td{_>CL{wS--lh`lW$iCCsaP zMTHv&9tnBe4^;0~K4+eiqGdWZOlM7l7s(%{ z4?Z4*ppegv^M`ZVryK<}z|@-I*Da`^OxR z0=Pq{phWFGo4o%Gg4kW~qNE=Q(~1pxUNwupx42QaeHRuQu5dy*5j9#-2oIPyahvt}YEyOpUOPzeSypfH zkp6O#2H~PBOJwi_^BX1L7uA2R0v(QBic!>6dA|XM zZLw<9Hls>K{(Fdd=oXC{pws*t(o}fV1gEnlw(|qdFKwaRc}lNjPp!mL(Tvg2W6zHQ z&x$G5?opN&S98=i_0~&O+;tIK4Uq7qm3|$@#7*$N&C#VdZ5$6B*!m_N)u6wG`XZyX z;*Z!9iIPgwXk5PvYkj63dw`Ys$+015Pm*VSlHlV!bptnPIbA}O#9V)RLuvaO`5Tlm zW3lJwC2}&%{a{00lK7iwxuKI_bYiyOr@kkN%?uRL-ydicSlqQ{9E6GU^aga7g`DUo z3+>;dNbdpkk^mKOALwCpl5GKw!OkPvFRirQ>A?(#xf2`w(U*);N$>9y9${5Qb;E_{ zP>$T*->y@qfKiAZMvMz;jVz6ryp;Wrq1CsU(!@cEkKDW5t8E3cbC-Rqv^*(5s(r|a z(8hk-sMZbxcM2Da`02@RQn=un)1C+!ARcr!#&o!{SxP}8&^h=GPus}&(LK?w%r+CE5WIPRtoD{UCbaTbbL>N+ z3}*1RQUnrkW1@ZK1469^@(e+l5j0UQjhdJ(cS(yXh;aushDEq)*);Ktwb3Zu)64Pn zr!E-mq;^I2v@j~tjqZJ2;J5fgj2^JnZv*m496Ci)!41sqnFVM9(Qf}~R0BvFE%%%` zg1H{yTTeKc*E$~2)>I)@E4cygsKlTy%r|Q51#H6Jjt!@axw!xooP8n#1q?4Xa9EQh z+4L9`2M7L~MXgJ$3mwtFQj|NY&-+RiwCE$k+gi(G8f%zNs4NfN9>MsPD-@NzcM9%Ci&Xvy z?aeyj->O);I8)!yItqlfv0y$S?+)b095Ga~?vM`=xD_SbTiQ5!^gEN-_mH0>3DChl zeJ#C~TWZ;zi*;9++0?_h`(k}3`hbrUZ8u}!QoMEIJVK2*(|PYQV!*~<&GoX2-Ak6L zbEbz~iXYbKH6^MIk{gH??(k;^_>HVOWu*L`F!!DzuX@hO!G7Q|0R{7cY`0eAK>N`m z3)iWm^Wt?1=K}DbuQOq7Q&;AJVM#qSql%`lsH?L4=BWB^Kfsqb5VU|_32#`t2)=dm z6b65$qjhiJ3!3L&bsTQ?IBtEu{12&NuoA%~<4uN9~fiROSO?Y$8*C1gYNo-$z-|SZ*bD8(~VwLFkXgfvQqSeIP)!wl#^2Fwz8}%I`CE3uY zUkjgNBoXH_0L}=%A1~qi8IOgQb2X#-ko#dxd%I!FO81Vi1E1~nxq9cHVbNEzP@c92 zEw*_|!AA+5tM5f>J)&RTefGD+c^8(9X?`I@wUXL0@mm5e4J7)vE#%MAe_vrh??35S z1l#4VKp0vlGFO86@B;%=GqOAW0igm|#D2UmhU?$hbWVgK>-%4vsee0!oqR1^Kx;q# zdH2qozD<;%@0WkebYQcK8I7)2d4PVz^$b%-IYbj@<+W!XUXXC$W0B00$GP&_AO+?_ z!(FL{>C@^ejokKGbXee6>Z`K;*r^X$9;5tGlMR@ER=~%VLXXj(ln~m(RV`uLluxD7 z_p58ZMwu>)Li~#!*6@4@V9A+A5XglA{6nT1dJ1$!@6T`@maiGwtqY2}$WzBTSee!w zsH1ND(Z5i{MOk}MA2kctm>$n;^Vk&0kE703a}L^!>B;5BYFZOX0$^%1qr1y9D%9W;xVFGXhkkuaJLh_Q3msyPQYf2B zv7KSIb6_Rw4CyXyWUvvmP=za!(}12<d-qoPnOF-)G2}1FG^tm}Hu-mXF5y1w1c28 z-O)8JYCLQ*$MzZ;tgt}h?o7}T9+MXMRAZ$vt{Eh0lBF&%^Q_r)!+s!Ca0DawY~tSY zsqF;Gs<2h*pKjpi`Dne(M_e-_(e>ihttm%!qN+u1n||l|MD9qJXpLu0c*zI+Y~KBZ zc}kcZ^r)7FJE>)7=XQr4AG=z``FXHC-Ku1N1}su$m-^YrVEc2^1Cy!KHLqvkcJU+E>DA;nX?k z`26_mFAuThi1o6HYXVhatx%2h$A^7IQcf5y#MeR7t7OwhRWOvan$RngJ;NGc3Cf-a$EEU|kDaI{iDmRPvQ)wkn7NSIR4nkS#OpY6Wi zc!`X`-FE$4fz?A6{QC^6w+Gvi_S-Hd$2(IgeK+^7-V{VJBec}7^$CZ9fox0l1)ZEb z13739m@T%nOo2g^o(LJd`!c#Wd%!9it~lt>Hu-tBi?+}5=~_#Gb3eS;jC`$sR>n2# zsD(>id~Nt5MXNO`NipMwl|av+ntCH7&YIXw3(VF_Tpqw_OQ(7NL?u}SNM!H= zI^&8SCylOYx4V_RjeGTeR`JgI9RhW5{X7p<{o&ZkCDc0-1FD&*rD~u2(LnI17vVm( zhXTw>YVe~qtWHAl9K|UCPe9&3OEje6{z}3@EtC2?DuW85-NA_(4c0%Ie8-9PHHV9; zRjw9{ptM5;T%UNFe{e0lrO)jpMS5mB-b*RXIG$C4(^fR~E?r1Y#x!1JHEk7d4Z)|> zUuocX#n(sMbZ zL~s+yvU&v2JAw@8RXlIb%7<0~*5xlSX$bk*g9t~fR)H76&E2GbCJxs%CjED27S^|v zgW`a~=hxzbpc55VfQFfLe#n06K-U=m!V)fX(~P}6mt-#~2!C*VM325A7!)fs19tSW z3H$=dMU&mP{CHNh1~ms`&NsiNguR-d1qo`jYk~Gey!~V!o{8jNvz%=L)cbiZ1yGDx znpvYF3f$g9zu&7(s^H~_w*-FooY1Px$S`mGhEPRFe|$Z#v*jT4d)7{^n({^S4Vq20_CdE< zP~eQZGWK-fsJlA)n82jlyGR!P=^Wo;5!60 zr$wZgu-6BkjAmtkdHNOwq9ddx!llxVqG!_4ui$CjOzCL3G?4zIKZw}nH{#@WZII)g z&3$&w)4nl>&Ur<(S)k{92H!;DH}(q40IdM>=aOA~1fsd401<)<>LX4HdS1%80}8(P zjA}PLN|F#7UR#kmW$7|}ALZs!fv|Xsj#f5F*@udH%vxU^M5WO0W)@GIUY&hhZ3oQ- zh=&7p=f2W_?}h+&vRB#hqPOB2OD0h?2HQ`Ky(1_aVe@Kex^D?J)X#AN5_{Xoc1Wup%crR4_+GAEV zD*cb4GT-67FG{JqDi5tvTN6?nXM32G;#8$L##4Qt&Q=Za3g#bQrQSJjr&4`WUCp@a zPeF122k9|UH}D(R#iz&GTOen5}=kAFm32zoV>?!vXZ^Bb3l zkGKBiCM3%vGD0c_NnOY=k~;DCMiTD2a3Bl>cD)UysCZ|S`8dCg+M9iE&#C|+80_~| zocy}Pu&UVd=RH4Ke6E8d#`DL83Yli!754!Ae>P7lMP$4nRmem8mp}P_$0fm?ev1jb z#X4Tt7JOnX^SW; zAKV;YGB3(E`FrHm|7!}vO4YhopVQY4fe`g5$HRvm; zUwo^PgO~>Y+$)|epRRYXQ7VbiP3nkA!mDs)WMxn(2e~LoqYz1A3G|c^kZasU^R?H% z5nNP!(bC!a|PovYN@@tavhpSS_v!XzO8~P+!I9B!3>c zt0nl?C`tYWs9&aLh+4Q<=T!fyK@`Ry^xH-^V7=0b055LYq2l1M#Q_Fo3r53Mw_cjw zA7NNSuCf-IULjGxk5h@Z3w`Ikg1j0$L8bRQ*G|Nr?RRm#hBC9cI}eEa!Q?i_<7@80 zk1ACJiYI`_>$UO3a*w=5kovtut>{JC0hD>jzIP45nJR?EdVWB#(*5XX|89%c3UFGlSJ2PgL z;VYNNUh3P7-5|0~%E*?F-a(J}r6LYs!p>XAA_(MxvD?Xgj})n5bqQ92_I_mFj(;Cp z1_s%;Rbn9~Xc4l@-})A@^3}-ThusC7vjhD!vZ8B%>j-h3Z@#apIKbi|uw(S+A`1!v z?7PB^c}#pf(7dJ6tmgBTRc7~__gn89cI4@1ALWc~$)r!*W-sc_6^S$)J0UUDgK^Ib@HF^NyU#x zMnb&y>ZYx(OvC7QotTdYCpi$Rg~*W&G!=`x&y>Br8-3kqJ2mF_OTBCoc5f0GrjwL| z8V+@%QUr`KQ2eqmoDNI{uf1p1%EX=J3>=hh*-%Mb?>2N0#rv3OxMDOiCO_3jGpeVX zp8v3cd3s@r#El8oxOtQAx$rGYXFa4k-gDhSc_!6^^cBc*d}^~Cd%O@o^|$TwMAJeL zI3jLe%2iS0I$h#6SXu)P^>(d)Ll2;OJ>8B^WE=P7?1C&7BBrXo(AHcorKE=xtM5=PdH+5{ykYIMsHb*=>Gz&{D$ucHLN{Rap{|nBM4_xooxeABi-TsmtF8+ z89e}Z1#2HS|Ks=d64UNozW<9=fZ_1J10T0SDQf2hYv2!XGPVLSY}AcP@@MkwPkxhZ zMDlTeNh<&=>gT~uB|YlH7JD>Lvi^N@Nn*n81<3Hlzs)H}{^cD7vZ4P{4#NIrte{rr zHZF_BtT?I{by_}A`sips%*bf_lWmQOXZUzR&$)@9@b=$NqMH^^hn`DrDk764?M(%> zf^P#heD1$)$Ixu=#CVc z4x?j}q>OfW25&WQ*uAE*`d9OCUte$7b(_ARZk|a^uLHm1cb4M*Y`eG?!1Ag(_K+5> zkiOTrHZMXOvaM}frg*g}hDCnT6FlOh%Y&5=&2p*dO8m#;f zGOtKyV>?F8|Ip1Ro33X(#SZ74G=oj|Ru5Gdc=`%gSW9#$iW|j0DS-re;;W4&v0B1W z1W^RJRzYb6pjPvX)VC385=GOA28cOC=G6C~U2QYd@jbj4`GF~uGu3swUCggC`khA=5{nj+(XpnYNv$b3NTfB@+fb zbPH5m#}9bhHmYB~*bBTls9odh8Kf03%xf6~yl56-jEg_q0XNQe93I67Z(Q_xkkDbj z$`!Yb=2PpZ4{Noi^_r*)(-D3e#JPx zIE|K2=|=VM76F?vRdioZY%Z%7WgYkqx(lkV5wQPko=5KRROy-gwzLu2^I89JI`4j7 zqJ;KQIr(mt-s9RRm3>o+>`sUZUb4(CcYLg6X*hq z-No3(@fU$E3Qdcbv~(44!WpXT_^+LddiQ*ziS8HNT^@HYe3Y*WD@U-*pA(9q>Kd#u zo*mFOLlSh~NzJbgGlVTOsFnKoTP9LBp~ILnFEo>iMu)9l<5*p&t8vur^%tkyf&=qd zX4}nl=_G1oDuCAg<=T!0LIwTdS>);Zy2(qGGc>5~UZb>zqEr&1re_ydb!0fVld=;f zJGd*0??N1(`OOT;zaj1gX631m3LjlGo6uAu)I>V8q=L=)@eEsq!NtYJ+xi9Dkv+61 z(QwPNdlhyEj8r)R_>_EbVC@U1lHs{?JIQ;2%!q@I$7=oo({YN~c%DimF0(P=vCZnv zNst~hn;Zre^2V*cIEU(o>#}MCZ2Jv%Z2)8I{05j<$jbF-bN&O5F1nt5spcbGdl*8% z4tP)&*7J(2)QgPBuRMyfgs22Gt0X6G-SNRJg+a9~QUGB@!FlA&%pVJ2+uq?e0tcat zPI{KegzvnP69PR($iXshDA9JIr`BLxo}jp4UxfjP>7tpggwQ5TV|$ID68kEBuuUMj zS2+1|V5{g!!TCRXR1@A~Hf-Ng=mzUw`XD}_@Kxbr76~BnznqQbFunyY$-V&JK zb9Oj;)$bl=_yaXC+pg2oMvJ{%wvP&?iZwoo{01`W3@5DP2q)jPTKPI{DW z4JT5M%NHPg<`bP(@_jw<#zxs}0CY$Yb=9@uvHBR%Th2M;?Y@tp*_8%%vSRDN#NJfHnPl~ zNa7}XsL1#@YnOmT?x|cL*ZTGTDeeZFt8(<&Bi2Y+RI)IEw_`Q!`;+@H<^dMNJa(O|Kq_=KyIm8ez;$yF zzCeX6)1qPF$`jxefkWFbww=MXYI~pmwfmh8S6-O!uM*?y?Be}UKYSXq1ip1~BAD8H z?lC)1JoCiJ%Fp=wY&-wlae-|7p(-Qllp<@H-kyjn{WOH&E~nQ|@BMZelZ(=v2$p_% zs#EyN?A+4_B0Z7!)-(;KZVZZ4P9gG45F%%?c&P86ua?4Fw^?J#J?{89y>`w93i(sv zsHw&2D!j}y7xzbL)XnJ!b&(eM^;*C?zR$cX#b#-zjevB8(qr5Kqw+rhZz4wscjkj6 zZ#ju>SbB<9ec5|LjNHn{s=4VvtI^l|?h5%dR~_4X9BZ5TsgrXjCmmCNZ(7xz7urE# z#zUUkNG~#Iq#Pzqawn~PcyolEZ_gNW?wOZ7844VUS1Hg&cp;HY>7zf{@P@YgmAChh zpH;R~(LXI-v-g5n3?o z`BYS4&8-Z5g)<|>&EL_sWRSx_tX`Tba1c$$5Zj1M8SQ*kgI?rB_YP&dK*z3BW(k6~GZQc`cJe3o zq1prTsqbIN4hbrXv>~GERoa#R<8w4RX`VerH0k^8N^t+Le>Hi5J7Sg!h%B%k%Ub&*O+2szPLtmTZVz}G z6MwW=G9ncH9?tK)6mr@{MqSdNT2F1L;)9i>zn&YSRuaQfxbiB`(`583{H5%Od3oB0 zrgGVrLNC``sybLZpo)Zq5APO5no?pP)@KHcZIJcSQX`91sDe}>ZlvED_TBjRj8p2y z$usbSbc(Z+643d(73S$hp*!)RQSNh(Z(XEvbOF5218*e!kI{eXqYe&Y{?ThGl`}+L z0D@PwZ)ke*S^{akSNHr0BUH1){4A|Ig**y`)>Y+O+zBd-I>od>%= zw-dwMpjA&sO>un3#{ox=wE_%*+X{*6Cf6+v$}b#n*51<)eJNx%9zw` zblLGF%aM7aD=V8uh-;wcmB-fz`}=A>lEbMPEe831AMhEa=x-_6ajFULH}*G2aBm z8s1RqqcQ{N5%>kF>e*^zHgrS@i7E~yYS;ue5Rxcs88eIh7ez*JRv=vOd7Hx}g&O(s zT|E{$eVLC(S!}$j0d}2foHKQ>4y;~WLwojv8g=S7B7U9=Cv#C_PVS$pv*L5<&8P^% zUeYJmSTBl9M|sA9IB!GtXzYN;xqZDV4qG8>xL!g>Qm7mJQYnv1aQPV_swru4wo{t4 zsbNJTpmwGs9l}97OFQORBVh9@x$bJGd8Myd5YJfMjLIl+xN8?g95j8!Ok0pt^3<$S z3nlAxidMDdgfj}r`}0GKZs#_vn)j8K$AU(P8ePOrqN9^JRUEQ9d$2l#Kf#gOR7sU> zIfvYPIjhI~NTes#C|?-GVBf>*g^R5^G{A(uDf*%gcbO1KkzRnEZ`RbOsd10XoEi)# zU6vo+vvXk7Z1bzZWIqr_0{3di(Dk{otp7>WRv~uJ{X(_A#i_~@isYfzn!)!0I)r{U4>ScE*yFdT_HkoRaRlS?C&jA4;7x}Y>m*Nx zI_*Tp-1+(=&k2WW)S27x7r3@c8+u+hyK{kZoI2c5(<9)l+azdifrgB=_o%N1pq z9)5(S60*V&^#pFp9(599(@K8ZQ=av`y7=0|oIkN17m-5bVT7F9ilAe9d&x*(Yu@)?}%Bt&;xH$HD>`OhPKqp%*8nvG37W)T|qA(rM=< zjK5A@Ji#vI5aJKJHi|nI7BAHc4p>g#z<2iZe4`40><*}wsDgkJ*s-u~l}XFyvm&k8 zMqZBR0?YB1O&|ikK67<Q(s5as#)A^H^?h?4oe9#izDEU+Uc>11q z7&2|QDoot_8b8Kqjfy!r;Yx$Mv|B{dC-zof=ovb*{`h;~K1dU<66rn~v*)^8y{r{|r@*GoLA z(Nr>lv(=aZQR~c@QTs3|bpd&Tpo$Z+PXO49cDk}}!6S;)cTXAApl+D)@arXUU(jLD zM{O1`&o|DDRCktB@US@dE3)EUr9cFiGp;T24uEDr}{5^?so0S#{hS$a+ zPN!=VPxTWzTLdU@GK*-T`evhJqSgjKhW?QA=UwXp^X#1_23cLpHHCe0Fp+}V`I>Hgc`D7 zGB&R=y3Ik?LUIwcV(BIGu;T63FR-a7GLW~gWFzuGBJ)0|)^yGgp{@@0=}TH{vgu*B zgg6Shn}4@i1?c%XwpLYE4MPBm973QeP`)m8HqMtv~X}B<);qDQU1i=1*pnWir_X+?Rt^x*_7(&XD z90Jhy;|uW`Ft@qmoTT!5<~Irtl5p=GyWNt}FZP< zl=rcWWZwE~eqU-iA0jA2$0`az-E`oS4(q+2kO!mNpSv=o1ncHZpj<-@{Bar!TRF}8 zD1KI|g)T~8wr}49*qF_OM8q1bfOMifbaE{}7XkJZy6X{G{~O=bQXfXYcjMr_@PF8P z_durm`0xKJm5S)-Y;(ATgb;FEQmIsmt`1I1NJ8X%+)}bi%qfQ)rkv_3MC5GBnV3_^ zVZ)r~G-Gz&{hqGxec!*s|JzzVdw)K!*Yov!K3=-%YJ=ZA$HH4U>C+s?jkx}FcQ+mA^cK!6vD+F#f# znG5jR}~T zE@Mce+S=hBi=dfv>cUIe)#JA)E7RZ6aXl;3;fm|A%hQF}W{wd17UBtG%*cf?y)eah zL|U#?Hwa-{^*~`P``F6HTFZHTN<)ft9lzj==h|g2Q?5J5i{9k9duVe>s`uRTk1byT zUMmH&>b8*7XAJ)2R8}-;q^*Vz^PP1z<7h<>a)K7??X)>JoBVimrDx^H)xFVAG02SX zZ8fvb;-9jyF$(mkxEF@!)(>?UPH2n^Bicq}&t8_}Gs#uyfrEL3h8gm1kP?0vmX6qD zVV-b91ZNZ(=o-_VAuXwAcWsXlygsMUGE7@2LORb1->0TKkiP|R*I?{~96wQ0cxnbP z3~UoO)W~5AR)O}UJVNmOepgu3`KeTjKI)BkgwGY;54or@KQf-0H(W6j8eaXg@RtrZ zcQe!-KVm?bXALv(z8PF4aRZs9+X>E6IGyDF&MY-F-rSTPQ6}Zj0Ajz?keb)W5e zefn;6IJU|(1_~h*ITxPSgGWscw@G4{kQr3L+E0t2QD)8apy)L{ zO=pma5OqIr1_>PC(Ld>=BOdcEfjNGMg{t__%i;-A@t5~77~PRg#YfjgH3}H2{p!Sz zZ!&VpU-Ahb-1bf)r#z-n*&Mp8QSZa?gUI*QM^Fy66JL*@QB39+d{%UUo_iNO$H-&Q zin5_+`>meHZWvQ%MR92sp5Ew#{`fd=0XcN{9{4-e^y{%{+DEmWqCd`q3G%bhE3N7d z4`oC{Xz#G4mstw3QK?D0p_i{pT*Ny4OkJTV9HBgEF7RDzkOa<4Y^X>0>prQ75i7?) zyH;1Ti*&1UyG8B<{^>e%({uLPn<@PhLdkpQwwhQ*+&(6BY-;tdWH3Nc=KI-a9{fL& zLJLY(VNiFX=c&fEv~9~iN$ZjGYkL%gjN0?6cWzy_GrQ_SX@Q#~`8bC}wg>2en(KdQ zt>SCH7aGuLAO&U?&~1Zvm#pS0%Rb$>y*Fv-oqHGOY?rZ?e<1#AWbWp5%S_WV%D2}> z!~MJAtr7}$+fi0085}QU>qJYx@8TRw&OFP$_9PbxMv(VT4ZejQ>MLR@!kM=&y(u&7yWl={X}N>Ew9i^2-(oTO=V@dI!rD zIL!~A4G>x;KLcYA!J6DGwKTaXLKvtEg^czF6bISCp_c1;Gr&~ePds4|^Sz&JGIr@i z6V!a&O6NIjM!tPY266R|d)kWT$i4&I4QAxIB?X#%kol?-#1Zl3B>vK{>F)++$APv} z)@K%yp8Z@&JZJx7IZHm(v32}*!)?7NlK5?J{0YQMm0hObzou~v4<()9sD zUimcsO?=My4tt|~A(hm|UniVmt94qP4!O>S`kU*L$NmLApZW&Pm-lkxFMU zK-rEg{G~?k-GwO~&_};rnJ=fZwJ{jqG`Ds$x{=?bCG>jVgCa6*FDoMmGNSrL8(G!0 z`g?SR58@~Nuw528s{5VX77AiZmxs2opbMr&89lzZ6 zuB}!e5ISW4<=jt$dr2uc&PsT#NOp_4n*`7iS*8;TJb6Jz6eV(jnd%P8*o?iK{mE&{ z^xQ)U*B0-qC}vQzJufz^&9^2;XmT}Hrsarj;Abo3WqN+d(yx}H2pD- zrNhzuZC5^q{HVF42wTgfpKV`kQxD>j$~~9QukZ2UFwVgwf*&plJKjwQHzHajW*me0 z?_&n(H#gjOv58D`eB!6E4|^jum~gP&qy;uj8Cb@!>-mI%g7heAoAI5n#<1csm>pI2 z1PMmM+PqbStT2J6E4&3nHAjs`2?5O8#msC3lC)7cf`LH%O9(ZQCCInc%Tk*z%-MUb z8iLGdmC@cn`%5mc7;m?dxhIN6p5qGtMCz_LPL;7;0-G|=XT;P*TDL$_8;&QM@WcnN z0_ouu&~SV!Gh)?`NU~ni#CUNBAKr;1re|BjF-!3I9(;K)m}CH<)-?vK3uWBU4$V z!7<@YzvvZUkY{vvKtlc}X~By*{NGx_eyl0~AIYLd10y;Qow?)N7}!VA|Kmg8g&Jg7 z?|7CvWssd3*J=?my#B{m@&7g=+`X($yyO@&<=6%A4}Vwy(gRE2l$)+aR?yG4oS=YE z{>@(J15Ni(EbHcv7hso~yCf|02gLA)t)T9}iyjcwnxlB(?!mS#$wE;fZqI0rEQ)A1 zsdg?eQzOU2yrNl{p;WDqup*+7zWDE~RFT6?D2L#0nDm-11K+bQkKVly|FML*wj z_2kaZy!OHXfT8*ye7K&{)yPU@4xxhwP!+UWB zV%Jo{FC^U^^SO?b`Avl9LsI6O7=cWC?it0sGdOqDQ@|-qxoUc4I6Umwx}5HbU#Yhz zttM?EVqramrww25hKCgjuYBspm!oEPI=a_l@3FMY%DqyaiKx;o9_gbtmDIggPyPEF z@%StG*Dqvf=6P<}8uK=?-dW%<-j`bnd_Ds#2mT1$r&M9|a*+dPcaR{)ak{yMVYa?j zW4+D^4Vp(hV0@iSZd`?VBE<-u%|1??dVhf&Shp&$53yv2g zb2sv*sK>C+>V`^hoygJ|7YgUqPYaL1>_Y5=^oB&&25gVU-_I@$oMG=OwGKWf9L@kr zL?kLar7`A*`Sd=-2W+bi)_s_HzVjM1D%|ZnefoG*vxJhU%Jv#rxaL4QE~O=Df93_V zI;YPXgvTkFL8m(!8xE~$18J@Ke*F1n1v2nC3~GeZH-dSorR7@z)SwnIm`FK#8!N#Q zOt=M;2`5{x+&|X)$F;B?tO!gC4&3u^MGX5Fh&J8wX_{-1EuJu>uM+9Fz#oOz(AaR4 z-H=RJ{-HkJeQDX?%CdNOXuUw-(tK$j&u$;QYszEG6p_6|p)|H~C1uRm|u67@v?NREMq$a$tZ zPeUJId|asTjWE9sR-30GjHQE%*ILT_ zYV2pN?(eI97UL%VFjwQ$y%+oR_x<*AdSa6D)KQsf4txrgk%t9q#7MX`0czWDfhaC8TbPF+~JDjQuEbNYN$);uRER`-NI35_44iAxR2$-3Mgr8$1Uwqdcn>oO#F-k+elM|S*`r`axNwnW6T|H)%*$we~@Ur*H=f2J1 zmr}lG$KwADp7(7&Je?f0`SlQ8{$tR5=}ZB0nqc(4+%)2QxmN4(#~-0wv*=x!PHroNI^&7Sm+TSjH*aD%gYS4chn_L=QEoPh8ogGLvN%hw;AkL& zs(yO@wDDP-RhPznao|WgZ$_2gt?QkS_dKZdN9I6xTHGM1`fzfeCvL8F(XS+n8d>0Q z%S$nYl}z~5pub0p=SKt!Lv`z)FD9##B!7JDEf!!foVo~e@7VSOX$M4epjVH!o_@=V zJZuVJpTxIeM7wY{@FPm1Nz0zL;EDONpN3>NO&bs~b)M zy+PnJ`T-02gL|QZ(LvMYJZrenZZP%Bz8SZ3l|m-7Gkz8WyPEOiv5o2T+>9@IU zbNNd5s)#vqmWtgoMOnK`=y+y7m1-OWIbi6yuP;}SV@gl0bwGNmNE;pq%9EF;7K_G(|VtM!YF!$UxX;>>wx2?{oaI_aZRSZVEi;7^Y5kj7x z;~Bo+?XgT`J^!@fF%C$Nne9ay!n>Ufg0jQD()ZvElVH%jIN{?)aCt=JGX2gi;~$>w z6boRVT=QZ7g!XpP)A-FeEhOCEm?~p(x&XG`n4FbkE(0Cg;s4Tvp=5y)U?{h+=A2WJ z5{86T1jH5_xO~~pAV)5t^|aUgkFx>eJ+;&1rq1#o>fvOgwUboSWJMFK)r%_e!udf; z%jYj=-G}n=i5oQ&xWZ5u{z}JZqwfi9n?lZUyIU^n1ia^k&A@S%Wa&dz!mHOBeb^iN zthkr-C>}QBU&Cp`M>)E)o?@_mex&$gV{UwTOPJ7h*xYR*n(pVpa%W&M&}4Se!iJle zneq~8&3&Eg&$Q+G&Q>=Exdw-M@mJiM!Q5awh9qXRGLmZamiq8h>~7@**B8Fu)tuHu z_2ynQ_f6(dY>8)&o<0wxUN@65#F*Me3XJp?c4dsDvOS8 zpyjeTUwPrceNo*ojcY)q5O1JIr^(a0iEK-!g*^+@{puA*&LEV`QbE?Y{E7CAfe~zA z{E9DUiCMnv6QjLuS0ZGDT=@3?i8q89rRB8;mn|Yb+zeWO(tqfGhaoB#q65r*L^4NB z%;+^`?#&&9tq#XqEYM4`hfCW~AA^ie_YXgOxYe^xYphO?i?>EX)BCC>>>Vz-RanRt zfo=v5b93)nDyd*L-EDgLIZo#gM&&C~HTdsF+CJuoo57zDeWZ!(VV8HdBO;-y zcHEDqR}4pC=_W?7es}Z$5boC+QW3MS%_#+4Hl{*si}kYA?_V-k&1_X5wVWro0iQQR zIbj9Eet}47IKq{8?SfgYPjA;0QN6e(RNZhS6 zKzVYbm^zA+iA&;EBXS8(`Q}|Cl%1wZ(#ZGU-hITQ|A?}EiZ!KYuZc^qY zg#QaR_!0lFU<2hSivJYwqIipYbp!-#*a`h{HSBb1c${EHo9*N)Q48j5Y~Si~z4Cab z23)D*XKPR}#r~{USjM)>Qoz)Pf3vw|6Uug=7;Ro}+fm@;1S2Uq!9w@Btt;Z%2EH^K zA79EM3^>h4<}!CxDd`#=M-@t3s2=#zC=}ci&g%-shdJ5QYEbK35x)genHu;~%&lTA zL+D6DJ_u!RSv)D*#$blR7fd?cVt%sDVr5ZEh6j>pYFC6e-mCDtn(gI<4}7@=ZJ_Xy zS}UEx>IM6y0%yrCPN+A$*9$Hp(C7EtNHbXJ*;cfAFLKoy2n%kU`cMM-%e2t=~FAq(F?zmfEq^#1Sp z9Y^h)8sfI8?NvUN^pPOUM=E#p{PRk=<{Hk@X7IML4o(B>TGLD&eAt&s zFHGu8h8Hbc4G^kOEKQ-Lr&Qz?n&JN>98&U=OXvUa8Wc43x3UoEZ93XXm;TE_s5-f4 zY{zmJ3C1#*3eL2#>^+`0sf*Xo*3NQjXTn+)hI9Fo1HY_iZX+skvg@+hrX$rNss2xB zk|oVP7Rg4ZQ_(?v#BAo@USbtod7(RwoxZ+hXEP0Ue-m6s@?cfFii<`-jqIGsL z#+m#F2Dh8SrVx2AGaRI{nZLuDtr6E44Pm`i=XNur=`Zcs=JE=hK3WiIGn0Idp3AqZ3654nk-4e4x%cJm9{gmJK3>J_|L zq?Pxn{q*#>NR2(e&ieN8rroozZGCOzmR!DoIcG;xE%nk`$5gv|Qi?A+sW_xf$@8f# zGgn-+ct2TqID?mdD4sEFVdce94*`JOitUM;+~wd|sdF<1$l6cG`w;8$>YeJ1@a=2o z#}7KC7j~l8R1E0p+8iNNWbiLfYRRNGrq;jPH=Mzo- zXnhlZNC|4~6%L^V?;u4mxP4W!I}qMH-{!$v|5z6Q31LfJ(16{3>*BLm4}1Y z0fd1W8Ph$}lgYq{eI%=CJw1z}}srthhS z*WO*vp{(w*<8+d412d0N`ES|U!{7{)$7Eo9j4Qn(d9bQOyQ z8b6ePX|A}V%_DEri(R;PT1dZr1Oh|ShNOcpA1jT0$#jj6+i4S#%U(S{!MVCdWNWK$!^$>g%$v}pDG z+l*)3cmfk@6ByEIh%Nt%YX*3Hea&yhC@pUpWopew$erwlD-q!aj0U;`Y~b=H$^*6M zRI_IkJ%Y|>uiXxyYy7!uc`Zg0s~m6QEy~yYx)4ev;!C;vC5{+!P&;#$sPa5{)Nu9p zL;P)tA{(L#)`*`HK%+?v@)af->h9i*pHs}l0Gg+$6S_Eeo|-S#iav{)9hG!`7+n0t&h)1J`U1 zo%K8#8RZW`rLjDD0WLp~zT(8X^d#S$siCT;FPi>~3>U3%WHsy4ox%%3C?nJ%gC!D%#^-)P6 z+>ecUVvNVwRPxvB4!o)exXMffLqoavFg$}jmW;+M<3HhS`d%AtvJ;g}s2D5ocy#`a zw&gkN4jY>@ydKt2yB2&0N&VPSrrLVmc3n_vDl3J84c2CbQ@Jcc`D>9tge8v;+_ z-A0ISy?%%|w+JK7^(Opkdj3XW47scyrm&=M267JZ{@492m=9mO*%y9|G-szA#AwFQ zy`hFlSupi+j3?MJ>^f_(mJO7^YfixWjh(LhNMp0Ao|RbJ}oF&AfC>}YHOhgGaEzvYN` z48m?kYH-9sn!vW;PH;bzCJOwrUnU3+XxEGmloeE77IDKocV4|NFqQP8z{m7xs+i@i zKoj7H7?%?@^?qD@RwrHSiw^EFIsb-=c&&Dv{_LcL`dm&OD6Gm~6bD_wx!9DDd&I~54Z|RH~$zens{Qv`CU_2h5)rsF+`GjA&TuR zc*Eti@pA?+0)8TCKL?kKCoZ&T#Cp%g4M#A=8_Uj8zwSiuUi1Wc1~c-|Bqu@FF66%zr?37lLy@cpDey+-|%A3aN!t>=k zlWhULZ$p6I@2Wi=3qTBIg|S%EW*@Jx0Y6t+Xp_93DEEQ5(2Ba*_yNQ=mmd()y-Dl$ ztCfdmJbgp`oJO$}MZ8N!acs;sghRw-z)zg6HH=-E*1DLF&+4~OQ!7QocmIrq-~I?c zt?|(k-(hMDUJ~b`RP;Q+^G14;)_l55Mjc75E_%_C=+V70{cemGdqB6YN;>8tGt_D^ z5s{pIM-`6d$NLVSB3huasvIZHxPNeGx*2{t8ITzDHk)|0Uu#6S4@a@L-w#*LmzpM< z6`8?cU7{Of_sH9OtKqMzc9Ek#?P1WbsJ-olTLlEBbrMMPs|(jv*7r0(U1EWmDK}M% zOGqJ92wg*P)QU86xu2F;c_i4^<(XtAlOJ0*&S*1ST@cbJ%*C>TAA9=y>ZUK}S#6*G zLYZMvGMKqq4gibNJqv9=1Z$AIx^hbY8M)k_oqFYh@9_s4n5(U;+aIkq1j)Mieg;p5m9|6zot}*ImL48go8}wRB;Hm+ei#P-t%1l~MmHT--D}cT+{NQ=o+G z$ZCBRtek>OK}r#!n9=+KSd|6veM6vQhJ-QTY#4ctiZmFrB3z9owINc^SKWpBOJs@V zqS#^7eJ)}-pWZ(UazRdW?z^&=NB#*4`@C*lf{e$GcpnR8tham0Y!w_JU z$1tHR17Ge%#^0ycc^JW}?t|Scw8z2Kvm+F;0{!nWOPOn9?2kwO^`+%!Xi3qMX%B{a zS9(o36T)_Q{OafvUtIPi${X@eRpC`1PCvzr0J#+xGU!QD#BJ=zM^vrQ=X)vqlct}^ zp(BIv%x62`=W{SW*1y=T<660;V>wn3{J3i(lkS>$PB>Q{XVveOC8cnmqd`+7``i$U zltYu|8T5lSLC0$l^Gug9yl6Rct$|G>-OCa158uV4et_xp*4DMsme>~hXXgc*{1_~j z*@1tF8k(^9a#PK#Jl{Yc#QSq6T945qFbTy&Pokf5z5{00N74m1&Ic?}FC995XBa*y zvpPF+D-}zpy|sTdA*#E#_|u^SH?9%S>o2*@LdmnZI^)W!V>BA@wpZJ+=b~6oxqW5_O1}~?Ngsk zyEoZE&RjHk2nZlZ&2^)3N(*JpzCSV|~}O{5pM+#vLyAiat* z!jOK(tm8dJk$a?x_*tmxV4o#&ndoX>^G{BaJgfAfDC48Dw^m4l!(LC%@9G!S&ozq% zOW+^*9UZrbxGB|})YV+CNEG{9xTLv%o$OU);M8Np9Y9UzxhUTYO*ZtL^pfBBCAL=3 zclPQP&tJDzua3p;wAnF+zq#}t#Egy>?EC?(vH{71>Rs*lmo;8Q=IW1qy$|nxHcHR_ z@D5G_bJFzht9oz`r0t}|@Yt}fzT_jZ^4sM-o+$L}MDXO2?%Vtt&H(G&aG!tZ$A%Z0 z;}VfXb~bEL>B_^AlFXCCId zLt8P0gPN66FIThGBSAnyl?_OW@*6$mcTyb97gr*zUl7E*L|v4L8JudTsVc9%H8=1- z(ojPzk&)vmHF|x@`%@oZMMUSowbVL2OY&$Vn}F-4jzdu7oHMI#U`m29!h9uJP;?iL zv}J-9r?Lj79Ps8Mj)J8?g71#^{~z!{%|>9WVqTc z>w2|Mrhnd(amFa552|w8MN>x&Jy~Kf9s9MGaH-%#3tJ>-^>JvQ<7CC89qk99Q)o1C-p+(Ul&A0X@{j^%g^@fV&wXl4n zxCQHFocHcR%M&m&tlHNBq0Th5H2FStx0LK;&DDJ8Nk>3jygZ@N&+V%nyI!Y8owBbf zR%~3ro`h`9*|k>>SiC0WmV``#Tm^JbOEFX-;2r zPwse+pMhw|<(Xo}3+`i|Qf*^p?3<#$r6p&d>KJzfKRsu+H|b8*axXZo!!*$WoBZefFReM0+<rXB4SC@Z_h=tS*{t(h{V4F@_%dtRR zG*Nfa%Aio1q1SYsYCUWc;Kr>d)EW&wR>r9)-FoF6Xu8Q7j#=ZoA4>J@-OzyV;}qf-Fk zNIy~D*M5=(XTU8LhleO zXEmN9PYpd=JuALtA($NeuZdsE`VUmVPSh&w?x^^sN!EC6#Jxi___@-84(3>d3T}?x z3iUxrPwzvQpyk4Yd-%7E3P0rkFd1IcGiE+r;C5Tgy&#KFi*k~81fKchN?1kx;X&eF z!e*i+mQ2i;)Lc74F85(X3UrSAJlzq!q4pnRF=Pvyp!(g8UwTk*r<(yb89)mXiHo|_7Fer; zr@n@#&I}!X!mqkgYv13F&fNczc)fK}y+UfSE)(+4pie2K1q}@Vd>RAmQ!(2_nr<5c z`cSTn2S5+K4)@zdwdJ1ctDMQV*RXY|w~P)aIgZl@&ACZtn|WqIj^SHYgKB0qKtD}= z{q@JH%}b%hZlk|jOGZQ4X^={cjBv@_sCteU^;>aTf#K%eP|x zINYB3uQ#UN2k)>{q@AgAnvx6H@gt#5>;PeJSuHq%ZYgW$uRo%0$|3HbQsYoxQ*Ls6 zbMW#-*`-mW-+*qb_a5XpH&=i}V`3EE*$#kFf-Rl)cXkYpKl`)Sp?>3#L7CIK*o+;< z6h9{kvJg6aH1S|wvQ*y~*wevplrjJwz1hTWt8Q*4HC^~8p- z%}BGLzUw!4!?MxrH+YLhc15|9#qtke)(XVw~Jw+Xhzp=f}o9!iVkr1s^jyEnd zY)Q3l>PcrUGm#-N@FjW8kdLcm0{FuXC&E0rjcY~GDz#9k3Rd^gCz$zcG|!R{LV|Q4 zV^|X_#f;_XDDurof;q#oSri9#g$sE@WOkNuXIl|9RWEP17q#{3uA0K8+Y_T?tK70) z-*Qc;2{f&XxjvE^U8GM^V(34&Sev=q&g!b@1mCTZ<>89xb@rL!CEIdKEdUz`y-=RL zSpPka1TRkQHC`Clo(A!?{j@sV6z44nnes8SBY=GvrzcE~o)b|}zS{Wr-b{n!vIVn{ z!7oV#Zv9_<9}5zTipTUkCMV7B#PPDV;VPV#E9PB+b`-+lu>Sec6S;Q%ss3`3RNa@h zqJO+1_Y=QyIK+{R&`aYV>L(9`04>N`#&7EceG{A`S*eN}X@61W%NrA`u-heuIS~tT z5DJ$05=#8iJb6_BdD+smd>+W*Z^ByuptNlPJ8D$Hx8wgDryIMB4UPR#5ExiUhTc~w zySTFF8|eSHo>n%6C`V=&(IGAKG?f?{HVS22(bb+hKX$H6Q8+cs=^5-E`&a$zTS1A? zdtWwaKZJNo6=acyIJbv5ZPYABbt%hW23`CZt_A&OGfrk}bIIPoaYI=pPiNvF6Ps(3 z`+lp?sTBSz9#3VFX0N|`jh=LkwUl6BFa0Z*84sJuJ+|-8wcfWd51lEi{9lRahqvZd zQ0vL^tGf->cap}weG>Y-fr6TST!OD~m`E-b+va2u7!u+)6(bpRD(-zE7`f{m3f5+y z2P*yn6av;?Ihpsc%-pOrMVdm-4mNEpxhwQ9m_1}MFqJI%Q}Xyt1F0GHHQ*BnZaHx1 z)uE#s?}6OFH*AO-zjf1Hbo~U&ix*AM8?zhyDbX8 zhbHx`INw4BA>>MSFuo?I*F5HKE({E9F7u1-Xn&R?FA!-$67HMw%!q-k#a(3y+Of^4 zu*SORL=HON_$*#6oNh%~ao%7> zBV@+PJy!#Ypp|LRryS{pi$Syv?|b@7*)_>1|93?`$vrh_D$+f==$-NVZ={vKLZmN+ z-}(s!T9ReD z+qoQ9hThsB29x?v=!48p*i#E%!O+J+Rn=5Q#W$M_3et)Arf=R`hZ6@p6Ot z@1(D$u5X`zYB%%|bMG!`BRUiIh4lCx2UMyqT~lw>b!8)bE+m`A>_Ae+Lr{qjzp(s| zgDNI*%Our1TJI^Ob+CN^{mESB6J)FdAMTN;c(Dt%u| zKh3#P3b6bHRT)vP7+{2R04>~P%-GH|1Jdpteb?$q5JcR_`Y3cBJnWuW!^lq+CXz=C zmyBh{A@&iRwSIv?x|_SmF3y<=`#TxaD<4``h@;M-OhV(VCqV0-59c2%;ub?+KIjaI; z-T&HUbDD{6-w9<{H}&EDc4-A2Rz?iE-XofeJ|oH$RL1MD7lcM)k#@T2LX#IdVfo$mR@|=7W-8 zH#+1T{#>ly*&%eY1g%0YW?vcjYZT|vHf3(&JDs&C(=4O~_KtvI0NB>Bn%@Ct1yt{K zwKv~|2fRKm0&MdP(+HGs#AT+$+K;z7=C(v26Bk*<9>c&gzF( ziw9kWpT~l+hC&Kh)4)*Xz3yV_L0jTJq*9N?@57vjG$U3}^&=BLxCGe0U{)5TpSOPN z7s4mvz2}~fHhUsTSB13H9O5z}sNG!$4a(a6`K8^I4J?J9>Fq>+9?bpSn{e}TU8&A> zxX~SeuLOT)ePoJS+JTUxojww*Qqu06F%8=i;Ue?-^21mi+}J^32b()mydPz}8cLd+ zt&sE6GPpC)Jwu?mIs4H{Wq;XuqbbTOESKiLxxQv;nlXRMan~v__t<||P}|8@l82}? z=A{(=pPLrywY6lxa*hdn&KP!y>{2d*ouOKf6h>_eN_ZpX&03`e+U@%~ur-$gLuLiO z6)2=y*ANU73Dp)yzFG{HluYdVp}aFR^pXt2kC(OZh!83&Z#22wTf0gbCzy0pmPi zti`6SgH%HK%vbY^`I)jwHN~+(Pw6V)o~b7vhSB8Ce&dr&v;Z=|o}qiTlh{%u@MKvg zrgadIp_^$Yb0anwpEe64&_2Lf$#uYRbCU11=%39BL|VQDK(s6Dq81XazJXRJzxJL3 zAX)F##~&?@J)xS;jaw`%8iX_ zpUZ&eX4EMWdSWH=RLl<+_BQ7Iwyi;TB6CcRUARi~E#;145P_ekX9R z&07Q&_fQTeX?enhi-&ni3H8Vnz3Y~ad1j>P`6!3=_BeY7Vg1fWLG;|7;;*`^Pikw$ zB+CI`?N-1o>~oTIX!D5{A?I#pQaUN8fV{@h^D#LK`x z0)gm%B!;>_)`W2hPnu60Cwms|XJqXLzgVqgj(w0y+Yip^ySf5Ds~G7)JC|xmZmr(Z zL%W%Kgt>}jv7^uT_k6$0gHyZ;*}(W9DtCWvjg5L4QNW;9J|b~{914$m#A5`VNjm@N}B}E?K@|K{tXHKReVsXF9?W zZD8scFh{O5pjE)10e;M|=IV4qOEnh39v*n%s{3cSe`1%8SYX8p%tMtQ0;@%0<3jZG zsD}6S0)j48WF8;X8LVe3DYrgVJ+tWGBVaI>V`SD=w9J2TvukjCMTt3-ol&-*Yl>-A zZI~c-h&#dS&Q*iwp;YZlqd}wZMC|hZhu}@G>AJ3}UeOkT(MFi}QWwTzwd1=eu#3t2 z-8`3O_W^h)Ac;(P-E8%Y@_*TdSvGe>R)~5TR=QY4tHGn#o)k~}BF)U0DRhurQ8Ov` zs&?InBb3A59`1N}!$wN5ZuzWWbiVkM;mwiLAzymiS9;)lY)R>s2DgTbVS{(pj>06) z>7Rdz-E}-jOCsup!qnsi{WVAg#Y#Sb6n7zQLV$koy2y&{*_bndKOiONGl0&vvO5tM zK@mu?Ze=|;p7J?8W@e+QTWu!i_f&n#yitBu+X&-If50haOX;yLpBk*LeF42LRH?ur zuLH1M;8aDME3U`bf%*uwjTI*XhQR=9kmiPyUw`>dqx)5OI*F}3VBSx8Tl^0c^9etj z3=bew<7{AllI&S)i%)|~CuA@}Y|l}}PtLgS|GCe>SYPcAi2t#t1c$c}uz^VYY^}^B ze?4O$NBxWq_+a<5$_43gy)9-((Cy>5IN7nYpWJSj6xB~O@d&)IdG?6*ovNM4c&x${ zz>qfJ&-~`vpw%DpW42ZV5zPs+$-tlwOttE{m-iHs2YU%*rr1OK%~p*t^I5TfOL1ZM z?kqnazYbI1=RB{-NH*?xqbzFbn>>CUA?&HFv=j$@g7AUu2=h)kWVtvl%8M)OqSWpwM(_*+y|amw7(j?0r?#2`r?;Lc#j>$0EV9lSK2tr%Fj0Ctq1ra@&nU?%+|%qQcfRZIcSZDFv=dYM0hnWSM* zrKUYK)UYT4b{t6kQd>k(!Mc!lFr6}@e$1@D$I?&a<&l1SuT#0e+J>SW4*hoCy}Z7g zkZJ*Ti_Bi5ZvejQk z4km^C_OrjFf3-z@1Oi+aKU>85iNFg(rupO_4CWz!1TrZy*cuZEghbP+^0Ck zOrDicKiID@aTh6qyK*kpSUezR@Txys9dl#|X3TWZj=`Bg)_XnlGiV3yj8dmt0;huX zN7t1o_3gA}H3YLaU)R)!3HB>I%fvq5g4J3Ok}l!XGH2)=3?L^tPvMVx?9t1~c6#$S z);yF}oXxWOpJnxdrd@wS|6J8io1d`PF3rMZ5Vyw=I{C-a4zgLgp*JJwXB#tqH#?IHh_dS(59gE8)3VErBY_@MZ19^jv- z1mY`H>C?Ahv?v&CDA|}%uA+!Dx8HA;>o43ttdV)qOvA zLhT^Ca{W-p##Y{GVIQ-qo0_k)?#AgR zkYfayUuk)3&CuiOFT5rZGFU z3n%?=JJ)9UxAXHBCmU>3I=v5UteMWo`RuYkx**c`3$>RRaJKPl;+m|bkgIDR>B0w` zo9nEW7Q!vT#*+SX^|)___xt0^r~Am$DWx8vEzvH`y4Po02nG8*D8{7kC3(52g-TM| z{+GSpYy=*+u66S{c%tRgoo!k*WxdBm<2GbGOq!nN-yt!Zb;xoT% zR)GC>X_M_=+)wCN;2F^AL1*cAO*VNk*Y>AY*_7wGKsN1jaGTD}j=9-ul!+sI0_it| z3jzb37j<>ajqfAZeZ#oxYjYKADLGcN^*`h@U1$Xu)%-E84$&&;C0k{Q&Ns`Sr`sb~ z-gE!0oNE|7_d?y&5x+X?fe|Uvp&9fp)t)yQm;#s=R?5NIAO=;^_#gq=S+9pZrj(;t7BH(x7I#rY5qBYI` zdw%V*sUyL0YOW{zhoh}zEH>sbxi9iJ1D;AzXYzGitomGdq4L+7N;z(1s;#D@zIWP1 z96nmEly7`w8n$+x`DXqt0rv3Uvgv`(a@qq$?s`eT&mUc>fA9D#Kk#(cZRhs^8=uFGh_RUR%-tpb1OH9bKeNQVrhg8~mU0g|hP4b zn$hZVY*eb8*z0yXK+4G9{YIiCTTku1rpBMQV)Iqh&F)N{P7Pi^*m^8Hhu$Kn=pjeZ zWgB_GUgP9M+lR7~PhUSc&M&q~;RU7-8 zV&^2yN(^DvleNvsUn^#@o-q1{s^Gxso=p`PWOyJj#XVl_-zZx*qjSk>`=DI2f_l8nhqyR&|`+6xZ^Pm&lOXN^?9~{})s59o6I#?SCs4 zRFE9H(jp*5=^bJqA|M?Eq?gdUbfhOLNDEbZSE@?yMIa!Z(0i2{dJQ!|!W+-I_jlhl z|IAu>l00k8%$~hJ`#XEYeTMR&2S!rrQY!QuwJfV${-L7vDY$6Yqpt{tqXQd*q>D0` zN*r<89}^aZ(?HHXZJ?sjrAMpkAg?Zr?8Hp+Xm!+J)PS0C<$;4?qfqmYblZ`LN%Gi? z?%;T0Vs|Q4xUMiZWwXy^gk3OWbAfxN4-%damzR}#>!{#!utX&{iezP>)ey}E*M){= zZ!YKz6-Dbi38hzsDTMe$^6kqFkXWb8Jb#yV_N?GLi0!j{o`X8R1{-nosJ!n2j~je+ zQosq$=9x)V-hx=yarJx4_I~ytSIA^)y4`rYLh*?mO!)7(Ls*sT$67HLub`slt( z22)4>-(eq3!^;h=X54YD`vA~*ji{D4a*p&2A*^nFHvVy{`LWfw_dTg|1xFMI|I=&} zC_b=UTe=mWSYjdq1>pvhMd$>)NlHzG-SX_|kC9w;`lR)5{RVkkC(4F8N$0DBb-Acl zX@caZA)CjroPGNBhr|ok;<$jHeOJe)&qRO-H~Sx0MWMs1l0N_LU;1vOM-B!M6&+02 zi@^~z1??c1?{?&A&Wbl@D(|I)by|pC2n%_w(UpjG)rHMm-3OjklY9rACOzUBc+a@` z`)_&u*1ZCKjX}VMt5gBLEnH5}mT=kFdP;B$5x6_6sCSRnObQd|`~H~gmOj#Yyf6Y( zhf#U!k;~sb^qP2bL+Q3K8S+M3d|HS#Y?W3{D61wS2DqIjRbr?6v_O6nUiD8T2y2u1%-#<|_KxoARzwF=U$|yIQl}rT%bS2z;^S+&gZF`a_ zCZL_L8sl$^lRf%lV3&x>air-b)V!SvCJZm<$ExkZ3A!vaM$cz(2;EZSG#DyE#|_rO zclzdaoz6|MjlB!!H6D+TQzw@aOSzMklUbtBg376A#^AoaJ0-56AKn=}=BPPwnP|&N z`oMdW6TEETa8DV-7%PPk$bSBX3>Y z96aKhY{j4@RgE`d93&G%)=R-14}TwWQS$y?v8|#+w}_&YwW3yYEMl@e-gXQ1YB(t_ zMcA|F?B@(fgS84S!k=*S_tRSIJR2$3Zb3o~-vf&83Qu-@ORGC=>K!*` z8%`8%C`ISAQ+S!~YQzJZjwnLoT5>a96#kj_yA8b8-f?7RLiE3FSB@yIyRx*<$l)PZ zbcWowhicG;L=E+3NSv6R>EozN*VTs&DmIyevAiTcuwH}dEr;3CT+)h8KINzyOj(iD zlS`ma(l|f{Kk^1l`wg0+;FCCEqMfHP@l78?a%9m}Am92O#kbcu9noc;jL&^MaCHA- zTX=cuf@45pBm5pmsbI_ri}R2bND3Jv-=lwL{Oc8J>r50|4%CeDzI%IJD@UHVcjS2R zQxRY^QM)t-?kP29hv&WZY_Q_un<{LqlCVQZ_MTa-%4F7D2o#ZWH#J%AOijq6PoiI= zy98HXXU1o#9eK&OhS9vhw7@0oL`f?36jsbl2&I;%U949|kUZZ?ZinSb3vJXq&C0Eq zxgfrjGq6M|D|HD9m{9b47vJZ|RA^p|b7<{-DfJZfvbVu;H9L>3Y^Oma5CX-deJqN` zV`PxRw)tLpk4x{Rdz))4yPv<}RRs^cSXFM|VV*7T#!CIfTc8W;W6kGXBaBiujJslQ zWa$=_6KpvX+Da2o%}iYLQJ(K(_S2SiAuP3ATy)pWlG=nPaJG}MYzuFjqHpLhx%Q7H zyTba{p4UcE0QO0nex1B|^Zs~%VDA5GUvm_13`#!-zuyEa?rjzL3=U?b@Q2Qna-p); zrCpQP8+oHjeP9Od%^WkryYRE@k&;|7f+XO4LX0Tl;q8;f2a z+HT{G`YM=8gqJsaBjqC9GXSVD9RVkT&R!2X;;J_`>F(-tUKYi6v)QO(g0L_NwmeXt z?m211r^Ib1iL(wcDpkQs9TQNNMM>Z9JMY{SFbN=NHa3r%}Dz2KkcP_ z(v5h$*%EnpH2?cI@my|j7AJTHcB5DFdtg0Mmr7NtMUZju$b(x1@&jUxro9cojx`<6 zoTzvGB>Fbq8r$a910c;lQ05-_4S;?)L~}ZbADQ`%tkRFpgBJ7G)wOSx=QOwzQj$7rW}cfR~uQ2uYBgb3an*)&@Q_)s=4ITnPmc* zJ+~I=$M<_^EFA;N@1QB;nc1&R-CuKCsXa}TzgYg}G|JBG|2yb#1oA>8*9P}kEfpK& zkpNfFrQK&Xej|RwmU^|h@FDSHGN6@C0RQ`tj(ouSM>XSXL3M%)$1<3xnyZm z*V`DjrK6;<^xtKsJs4~)2&v~r*4XFbPN?~`#I1WP_=uq8zx)2fL81Xt7Zl@R3f{V`DOo*D z_?!oX7hB>tC#nm#B&Mm%Mu{Q*m*@Ctr3;&Yt|HB5IOuG!;mkMI@TJ%$cI3e52TGU30UIl)yUuGLg3tCmj;Uudj;2`zx9Ey!{VUigJP%1U{|)4 zXLU=e7xHNTw-?{8lz%~#@hY@YrS^UT>3G@pkjfriXi3Ua)tCY{7ShpoKo{x5x`vwu z1LJ~XUd$uOmmKATCpUu0Sc>egx@&{e??0D{XKj~U=u zL6=_EO+O!x(=PN$g1yXcAhcxG41Mq|$UkhB{~YI}`j2);50)-U>t)$RnoB!Kc_KL> zzQJyB9tnqB8{T+Jc=LEcNX5#+b%cgRp&vv4Hn94%_28@Cn`V5&JZn1&;|q$5!<6FclO#E}MV7V%YcL^_ zN!#o@m{74G?@uzD2o%oh0ST)4bu}cBq}-_XIt;ERMa!fl3*!t)mJvsJYsvEc0=vgQ zb!+!p7sd#Ow9}}N(HF`%{MiELEa~Fl(s`U7>YpujzL&PhdSenaWx|p&VdPvTvK_VN zZQT@V?d_1K&zr3eJNH{>TV#8~M+jEuNu8giVkz`V{R~+zZa!8ZrfNLe z%<9PzmpV)kCUU<589yxc1bFJ+`_nPC*QXCqC1HZfM%fEZ+?k@-&0B{aJD+-CcnpKl zsKGxAy+4!Qfi)84a$uR={;rv}Y3j*hXk#i>wg8OK@Q%9TQZn$9&`*J!PlYr3CIlzT zjn#RiMKJwYbj1ZTssshVW!p+D`k-H<Sj6E9Cv>$F)oMXZNyQ#OLjBCgE=V}SfFPoPO-rDmgGbF9b2nR`h;?COPwwgKV zlf6HI(mxIs3;A?bI-}JS*qtp^PyQ9VLH;siT} zW_!Pr(dhAAK1e|(?toE5YKlRLgBa)Q;v`D$suz`{sCR!Er7qtpyt}yKd%uq0BVV2bOvU6nKWd9TQi|UFVddcY@%LS+ znCI?rmccp<`nmH$cdatsXW>kJ0fG~^r(X$GZ~A_!L?0!@qUYT%Lb!j?_uY3V{OgA-ZuSOJ71s-d3vW;N#kt)e8JE4Twlz>X z5E$^~rh~E#$>b$*SJq#f*rxdF>j1~L^MDL*vIs!Z*kR|)7dwJ~{{wKTYVgO}{~&C8 zeXU-Ze1+SIK?}e#ctsJFwFQ=&k&Plg*UAKG>S*l7Qk4|Y^G)pF86=;J7nnB`Z3vc3 zsp%gLjc%R{p>r;`4=eH3es!i3;Fu^3D_}&_G9A332|PUmWBH5!@LJXARvx+Q*J?n5 ztWckLF*!z+2m7{mGey~~^!Al(Nm_wo3sIH`i|cJZt>|Xl>v9Dw%#!+($ zs3e$YKYMwQA3e5}2@9m0eptT?@*H&FWAEe=B;pXF+QX5XZWSuJZ4#Ft8+aoT(U3fCm$}1ld6lA>iIQ;$a7APOI>n3g$_x|zYgIW zqKor~lGQ+KaXZSC_G7+J?9S1DL#hs?7NdoMPzv^DZ1?DaEb|H-Eges?WZKE06H6gUkN0iY_&f>fb!GLPei2Yi3%ZVsUhO`J-rg zwmiXDCEsq^s1r-Zqmfmar^pJl9;)>8Pz=}mvT43|Q9ZNa>UjOpsfbIS$f;W5;WkZL8dC;lXTz+XKm&7Jw8LN;bST zK3c8TT29^&E3kQ}Ox|2YBPRsGAhJ{V+6YA4L2)%nSnkOsIU21b#5Ck_#w0qXKRm2L zI1x6oLhkw1Km5Bo4U-QQW9&kr4P$BZ6_&$CJ8nJg3WXpTzXp&?Mr7q%HigP%fsh~N zEA}*u`b>h{;p|!5$yQ*uVH0|ILQ?nB)Xg&XX}1*WF}Q3Gxz1aZ3-szL2ecPOA6Qt- z(U@%i>bVXa_ z4tSHW^ZnO`te^6Sfpt0J=ykpxB%j_QDk;^||CeM8`Dez}n9<;^Q-#-`S3!Y#<65&!cW#WuWlU5X_#_wIS=Q`UNy9BV zLm)7-vF{QStQ*8Fne1ZREa?8@EN?0^7wbR~O(*6{c`9HI1G>ZIUo+C}8$CtY7N$oQ$MiZ3OAToC}2jfk2 z0oN~BUqOzU>2U0CD>N#*XEM0&1&>qB7^YZx_^?5F++x2mrG~Cps#zjs>MjRY>uF^6 zW>(NU>0yB%WTahht<@4qbKrgj`s$4O>n!M7nYw$Uy8VKl&K0%GZO+J&DC7~lLvnv* z9yQ}Tt8A!pg!@Qfsdy|4WMJc?%CG-8T}boJFD6+(C@)J?B3B@`Cx)k9q1ZjWFyH;%O@$Y zdN7$R@bNe6Oxp{-kDT{i{P`P;N4RSr>WlqrLry3Ze4+MSOWXwyw;kp{&*n(W`zyFQ zX!b%NFkJ@*xTPZlaH^TDWxM!eaVwqQW#W5*>b5Qe0ZGmi)JYv?v1uX{iMZu-6rJ*xxKNGe$d09a?D3> zn2~KUp3VBAFf5)Jr&w#%p4-OPpvOn>a2>_tIDgig7zdmc^4*RD9yyaFcWBZ<4RT`m zhDnL%Cm7!j5uR&2S!AWA$|u}s>)f0j)qvf4*%9ggcp-ObA(fg@--%IxH%*c(8FHkW z{dkaCEVi(a*s8IC&I)@Tvi~;AUES>qNwZqiWyQnrDX$a8x3wGQ0miX3jMyt~BIBb0 zIzAMrwc3QF4~zi0cQuX0v+27={5+!E$Eh>=tR$Eh7g!q$J$ItJE??;i`@OOu>O4Pu z>W1iNZ75n&Ss!kGob;O4k%TZ6t;jFhf>ID5kOFPG!2y0r=C#7rz|CSO4ItRE$xp^D4S->MT_5fM=wrBGxF}{->LATdLy}puU%Db zwWF0uuAqA3QAaf1ie)>7W@WV<^QAzM5CoaeS>Ie;`nlm<#R+O?sL9`Wq+41C>+&cr zMt%dQ@zke-Kq1+zTQ&U>96#0JSAP_zhvO;Zfrsfqv+t)9w67o5-h}oT}ciT z!vqX${RFl4Z;D3me)4wYUs6ScC@x>0NPW6U&1hI7(ud<9%;H{I+MdNS)2brIa##?C zb&Y=Cz=TLkby!t(IL`q^+n)5l(2tPo2~;p08OZ49UCC|DWI82UN#DMR1ZOXEeBR!C zR7+ab;hZr-z{@-f z%Ig}c3<4HKN3mH_Rye}^#xE@NLW$sIQkK*BPIpyotS2{RT6Lzq4xCR*r#QnaM^4^{ z?lyoKmCdK8A{$hhZI<(Rv;QEbq?2UxIeg?{XhDLt()a3Tpi^o@R5bEZ=ez9afmN_$ zusSz8epB62(r%h?-GH}(?r5CT%llah=VIrT^9IuA?HQoYG*9#w{y|HlTx<|gwI+po zu-%TALDxFl@;;hm%+5`@?g7#Qc&Gpxuw3<0T#WhoHnESCc7mMeHT7BsG^1gj>1QRF z-e6>&buiY#3CYa;stwhKOuz`eN_Elmx^f>|u-^$f^yG_9HksnSR*P{J65uYMFbM}4 z5U@;6x=gO=E5J$!pwtjZKk~z~D)_@sd9Ebr$dyenM}HO%^?-kvBUxe={^55*(ZPw& zBTj2aKZ=7d4}tDarh2<<&KJ!?LoG?Nm!PBoLuL=nbwS}_miA-T+R-Dy%(qyeg47jcnVI`A>T+`U2stj5i{j-KxUdhZuXn(vF~vF zi1DmC@so$c4^Q@|_Dh>_0hdPtqaPSO;KY6b=O5||^}N&Hmej5_mqoc=1}^!Eo8ewa zpH8qRI#fP}fFXq?S>#bi0-$DSh{p*Il$&?+kV&-*_ijW*-BR4{>)uA`?g0a^mrQ0b|13d) z#lb0>+!h|`gJnEVp#I!HBuv*;;tF`mP83#`Ljs!U&z=}y$%vQ&nqRNgv1pd~)-@xc zam1IUU$2B_LpC=DvCO%7@I`;V+`Q$PgNS%S?GSA|GtVxeFtv~9*W6O&m#<~npS*jr zaJ{KoKVIf5MI@z@G;~u=@FGH5B9{6;a2A9~TS54T?Oa!1yqz^X*SP?$c1_U~@xf$n zW2|l<3}x1I?ebG0n;uJM*900qx07`|S)RO}WDE&fDN2M~+@H`dQdezGI4M|yMs^*m zbYZ`}*2P@=^(D9P6HCn%xl`F@6uf@4w2FFUb}fWr9HD$_e@gZWZw7mKvJrwwMwJQ?vb>q^iw_8Kz}hK*~4HyJt%gZf{kmV*t-47INwD%6OIuz$RSsSyQhz zN?BLpBN^sEgzNw@4debUB1+nS{YHF(`&V6gkMO% z^O-oT4}VducfTIKSpJQ|QOA0h@cG1rMaFQUpO+^Nf6EQ38|jLGMMwxnzu37zjZyC7 z`M$`_oJ2o{uRg+>tC>7q0@##D{uTdJwB)YDxDw(B@B3rQEk=;>kO?yS4d+g09$}R9 z7z(vOpkScX+kNW-+xz(&=V=CRL#-79--`uQVEx%Cfd5hPOo8v4GInkke>=a&rm)|e zbc2eaAl*E?Z`&a;X4_U>_vs>91D?2w9&SLlx+)9LBJi?6lZDK9l3`SV6}s)cZDHtk z5l31TBD`S&x!*Xp1UXIRVQ}(svW2Ix49l48hwGuWvk-lFk(I_gS8c_-Tt0FiR0@jv@w|5iA%>9 zx3PA(KwPduZUxgekpYCLqS}Uh$?Alp2CyyDz&3-J)Q_gTvz47*45#Xt?F_+K=n-@S zSX9}oYx_Z5peN6*X^026Ah0B@TdM$-LY|^^2lZ1ofH!l5?Vsl$jF~A5Ya68OoDC8# ztM^!4zum}_NTqLz&4LS~1fYmi)5*B_rsCjsd^4Nl0bo>&j^N7`n+&z?7mT%;kg z9v5t(i#EI`3;BW&@8Z-M=IPLjn(S(czD7-%kx-|%NU9$&@&wUzY&Erqwnwg<`~bvN z+`NTf#5nqyXRO#&^Df$E{Lp+j)<5-d(UDWOmC2i9-Tdpctxn!OIr)RRr7NxR%LSNK zg;%~9)ZWK)EPoc%6&$?r52G%7$`cxdUSd;53 zO3%SGa?3rD*t?jELJ^{oLyBpp5}xS70sxmh5kr>7f54(?bANMYt+=^FiIw($=$L{p zQs}R^#4QES7%N3^b?if^b#fGnLOt(tgF1V+-3O ze{$1EThg=EX^B~Ib)bznIsvwUUPQk+ty>(5>g@{oI~gsqvJ4Hc`B9@LwiAKfc!imE zmfxdB4)Za{q87wdh!FPmFz-H2!*2AvU>d64HW`7s7|QxrEiiL#J+JGkC~qO?x-mm2 z_u>kXB10HCYt$oFhT6j`lJZsUgqddB4#*UPf0-t8c172n+4Vw*FN1UQ8y!7lvZEmMl>Hq?H-EzT4rgL@KnYV_m8asRLY)&t%l!YEzML|zV2#M-ddHMQ+ycAxt9w7jG zk1jy$NP3ow)#C*e^}x+$-GmbxG?oA-H7nC^T1x8;a;D-L@0blAHn#<{>m<=I zN=!dx-*`!WJ`~!<>H4&1^so2_OA!Bz^aXlfGRyy%_ElFN31|J|Eyoyu(DR57>ywk( zKq!Vs)gq0`H*EXM7=7|JVB8fma?d~}*fG|vbmIL1qbm3Jf8==}gI6h|Ld@KtDDtBN zUR<SqR84Q$F%uBxT)DDZfItMmt%fsMs$*WdCBJ~A0cEs|R(D2#NKCdpQ*zQL(< zd{WDhyXn(buCYF4(p|Gj$XO65pq){1*e&E*#M*;EJJMop2nm3)+u|lUQ>p*`F7jS# zBzSAf`wSUfSK;#+LDJ;WwM{j5s*n z21F1e7&a}R<{foTM!uWclEc^VpXWWa@i$E=@xhg8NHmW5+O1n!t;zhcLXpqk=ymmA z^CGdPJT+{BBnk|Ck9ah`@NA*Xv@^PDDFQ%npWV2`ver%k#;`HUXSJ|ucjqL3LiU)? zMmuL!X#p6EqBt1fvGt zS}SBhWKYtbcvNqQ8*zg)c{RFK9`6wXOb2O0tl41_;=%J@v~tcB?G^TAIp%)p5zW{W zo9dp;u3)q!)~GB*@1Fn0!OGipwT$Homj{M0J-CPt*^5}`Z;4+D5k+tCQNpuY@&)0R zp+Rlkzb?ph&MRg+mS&JzdrhilC6g$dC@uPjdFzDPXSkHIynOJgcIijkYMzrvh9KLK z=h6~g0gs`VFnUhEgnK+BlaZns|^Nv5{yM7W9wbqzzaX}+? zW??{3*H&9Ia1h^bT&CMF_PxId>a+7ZRmmDE_&*GLKw(RMf3fM-gXr@&W?$42B&D09 z%U}LSsV*U_(lfyX&7$#XT7Mf>*rV>6>$5C~K)wR9xVzn-_Hso9&piSlmqteH2F{4^ z7!w5Sbr&o?k$}sd4hnalQRXufWJGCqa#mI0jFkssqi;-(IS2~eop0*PxMR%mk*4A2n9Zghi3{(lrN@_#8_$iEbCWZ4$W zPq}XKl78VkK0_4PUhuzSD@%y!zmQHp=7iAsYlpf|S^nevwkr+<`+ddcx@#AOANk&A zTax7Vf8edlLhAnQ@F=Wad%*-PWkio6D_6hAOC#njUr$d5ZI7{S3;YBv2kFYwjlZfl z8N#}ht~JTo^Ev4#^Nj)YS&zE9(#W}2n0xCBV>_8qZ$wi8^s}q|f@O^aEvz9W&9@EP z37d8^H&4&O^TI-cMVorzR3=5L3S$ftrKPXVd-cxsm5)ogc@CmtM{z&m~PWZa^n=dlQldc!)6Ckipx-uX;$$k3}wS?H(a<6p%= zmFc3E#^C>avnRWkY7UWuZku{gE%oZ$+k}~=tONn=H(tv1D-HTD^EV`(FzfB>6oJF5 zb*kcx!1|5M=B{{y{?Y(w^J|LA#B<;nZknSz=j!$6Oes7sN)9NSc- zkX3a$Y)i`Jpw$pT&Ro?~7~$U?ht+@`^F4Tni(apiLw`G3y1b3+g;9UKvd1QXn{z|7 zV4z^^Iw_z_D6m+j4}U<1dT#E^@-A|L+<%m$9;pnj_Znk zV8^=V%6F7Hgb%e1aXYp>S>Hg8*Bw&Yr}Xe;I2*r|4pN1si#_Pgj*5H-`;K_SZ&nCx z)gLS8YHE`GAc~_{H4LsSj_&Oa0Y8IetT{&)%Mj&pfjYIdbU&?HexaESmy@4BU|jMZ zt9P;Rz+H#sSD&T@QL2MuQ{ln-pGwzVGxn*B8-8ZV;|Ui=9EsI4T>(z8>X6&I0&It2 zBw4P#CThC602;Zjmh2qvq?PkSv*e&rkNqFko8yM6l5$=m1pVq;^?LlVbNS8*3d%ZO zia84;R5ja-Ug+Q*m>%etlYo8DEkH`B) zYV-Fcw1-|Jak!)lCYm(J8=bwgw|$7L;w66g-aTyA%@7Xd?EX8M5Lh{93W zKZsgL0ukc7FS0wWlqVn+Tf~Wo+JH`ikLo}x_~S{+tQs7Xef71WxHIj; zk+y7$kpxdsHU-|b@Yg!w{))7babBC*d%KXgW|^+8T??LZ4ZC?Z#@WHHj=1;b`s;80 zI*fX;j&m;`QtH4P&-!)$=Efd&<`gf?oW~KbY#&n@|N1FWkRs)sZz|Q1OfP2Ec(GfG z9OJ|6v{$0%otxh#Q+_EC;<&DG#VG#z?qidz*#G2cTt(~bfz*?GYrzX}Z6NF{@^@tm zdq|y|DqqNOrHW)^YW110AVBT$!TD7Ia6Ace{k=J1{lSHa(M1(jKx&qS>{kzH?bVw{e(ycfk63|Q2mDA1dc&q5y z%tc3h>kR22Ac7wJ3mFGHI4MQG&78ec91=HyBHf=amgmne*p$lK8aJMZd?lm5?zLHH zdwqt)83de;o=tdGI^ZhXWztNG0|5nZ*!zBY?_Hgqb(`+B`jPz6ee%m1q{rL(>ux9M zgsH7Mn3lg}IB@|*N9xY=N6cvQpr0GA?_(y}eHLYx$9E^dc=fh!Y|s8T{{uD6cvQ|t z_nCWQ2SIGHd@NazcvkXm(w z%IDsAwm=HJ>z$x^i6f!=A!>9-G%bnDPY3PdK_K#BN%Rw_Dg}NDg*! z#OdIlbohCleOaz&vLg#OE4!n-{M=!$?VB;e#cowY0Y-LdC$}oY^j8Q7-VhwX{FUwd zPYous1G1CYisLYZ&9q@`P-SIvg7qVfwntT1s}*^6fx|8hV^WqMtQvDgvLzRF_0UR^ zQ@2i>Q*yUOXDi6e-$ZlhE^M zG_+h_&ZePN?iYwQuxq|E7JkJ2bu9(fDXSyy@4MILHC_p9F39xW=MmY;f>Vsj%Dpt) zsqXaEX#EJGp`cYVOeRaaVaQvVJ>!!n@~KTz$iGc1<4D=%$>W1_&#LZ3#($W$0(-wzJ?r znJyL_Jf#Wgt8W|LbBtmWd4azei!z4(EiW_!=~#OHA((3UwN%YRC+ zg)m(?SxZ6Juk5L@hGv=9aWZe;?LEU9+Cm*Rf>v@#_2|&xG@tw`<;&F%k83*gw zts~sm{)-_l+E)ViCMhjU+&$l@d9NNU{X{+O33uZKtIBm*t*5FNgQ1wCla|eAo3;R6&cHu5&x z+Vq`&AIHPjs6pn|m1>qHGNFqakc7UiTBP&pLwWTwc(1stkB>tHV`)CD)2z*ipLNn^ zO+4#2zeTEJ@J+U-+9< zZXrY5JF#)p|9+k$O~ovu^7Iv=XU36yT$L(+Wo{BPe4AU@LIrldE7p3%V?UoZ3IF)m zM%#g}QSVK&oLm^g3V6UQ@@UHj_~QlCM|%FOQ@MVk-xa%vZJhWNGx@s}I8mcWwn7|m zRPOcXt$M)0dww8$o+3S2UZ-l3p4z+m;|KO;K1_AueO;xYeNNu^p2d_KD5RoO0+vS8 zfv3t=Q`l$1pP{chjGTwOj0WVNQ=He&^D%Xj1L9v=;=N_oUvS8=7J;@>eNgXMwlNYG zkj^la#SdKI9ON$wGh$e6uI?+%H+21TXUt!y&(U{s*jsAhV2;ug_7rxZwf2p8bWlc* zw&+*)7yJ3ow7N%w;w01hmKu7_@o+i%h? zc}B;0a0kd~P+B=eVSh=j^Ke2$!(DRq_~6<^*Q@c$|BYJWaPWrwEcdU98RfN$X{#nG z^w9nS?n2pfg+%?1f2_Q`$8{*iyJKdhu01*z6z(xQHx z5nm*H?Ejhc$E5#}jW5iqyMI;x^dg@bLS`w{GFq%gM9CxUf`p%R^drkS*_Avzc8Zwi za|y~-S-kfn)qjNQDN(`fA}Ci|VMi3BUZY<7`1k@Iqa2eN_vo*d7qWS`-TX~6o%2DG z1`U`y^59v6O1_iC#=QbAPbqQ||9}B0MVyeC5zDo)j)4~A)>lpgk1)YfBn>cSrzwkTw+9-!6i+B*9Q5s8i+BiZ%09q+BC z+B=DU9;m>p;H#za{c$7q zJ^SQcI;9B3$q3fry=zHj>BBJIO>AvGCs9`8{9c5U9A~J7+J`TOUxcU`*Qp(BCxQG= zo~7tDU=`o_cDg=1e1kW=QoLu)sOBYmoFgJOX@eyqmfkr1F^s>Mpg>Cn_-{4F!P+$G;|&L3i+e5RP=2Od+(-m+ z^!=1p{5_rUetRTpP}d7l!7>+*;g+xpksR~Q_Kxk8R5bC7Z%RUYwq;|5GEe0Objss{;Nuc#h#Sj1vNb8A+6D}* zAScrYOP*W4u3i?)maW~1ftfvuuTYP}FJ~*zuzvwgJ*vn2kRN-Nt&rm!Rp!r0TO`rw zzmsAF@ve<*8N5*6tbf;_7LCHnqo?&JUI!#XwHX`t*neE+4jf5pTyUU1;M&z-P^Wo44$7q1_5DhH~j zUcIyki1c~aPLJ)fVT8Yr2dPKeF1bj-^|H&&m&mte^=^bz%NJd*OvvY7^>DP)lt2?P ztrSj4iXh#1&)ln4kp2X~b1+~;Y7RxbPpH(#et}=Q4A}?5{F9G}>>!Y;jjIomlAJG= z=O10(O7E(NEnS4-!BErl+tXU9dhzUpWO(9%-~gl^(v7nk(jkU7*2zk+G8i3CnMrBG zgG1^cZ_-yN-kU@Liz=VO7}t#8Umz&zi4g2P_|a9M>n#;dWPP*cuQ>T+VAroO?vPD& z5Oexv+Zx_gK^gbyDDy)!4kxmAm9oAvE+FaC{cmkUM) zw~Z<6b=62Z!i@r`A5(+nJTz`mZj>s{J-=Pej^ z>T<643ioYI#Gro$xz7oLSNWL4X-C?aZ}l$3`*8vhSHrv>tAoc&=Rcz;gDhZQa~NJV zJYR@Obeo9m3U$GhR|9=+%7>LBAn3riJMvS;0oRt2kHnOuLTSYxq~2FixmTJ1$qs14 z+X$A7C|(urc8wK5LlaM}=5A1)b3~VRu3OB5S#UCRjl3uj3o~2_n9ROI`-QigV*^f& zyWRPt?Kb}5NnE?HUE9@9{Eq7Q9}W5;&lG#vSNI}mGGJSdyAEd1^Bh9iYt!3RAolbh zW~2%Yg(;uWkkdAiY7|Rkhjjp<#Arox3-x$ zHT+%A9Sr&eR$hHv>%2w$lb3ALnaO)x?tzll#ays&|MSW9Kl9F^Jq=O8Zu7nz1U%CC zXDfRi5VL*HZ~Sus^H-q#$8N4 zCp3%kv*hWsAKJ83*)MRl`cukh52D}@q5xDM5omU ztZ$%JaQLSyU@KQ}CR4OrhA1|9iCr=fEWfw+qFco}&rF-T*U}fpj^q|DsmZFI6rKQk z4vxxBqZ`&UL;Io8(OmB@ijh8F>png#WSO(|u>-HS{tATjCD<9!$#oIVIT&UjXJIMS zK?iPri`ujk&wwo~PkTQA*eV)mI~sUO{_!eLowLDeQzUr**r=<~8v=PH1-KJFbm$U1*foEWN@HQ;Ef_`RkOTFgz87YgdYw`Z=lEwupwzFHMjc-bfuO~zljT-d4|VPBbWc+`uMx>S%giqWE&G40BeE-~gZ-Oo)t%HY&d{;yz;hxukB;(dk(g@|2&N9}gd)x1AE_n zCn-^z%P`=8Dek@Vi1uAqtN;`z8Gfj03CoAr1t0l`pHH{Mh7P0tl zWuC2Zr#V#cD>Qxv^3#oE+i*q&+S5A0)c4}SAvtdZkL7eA|0VSX3*&4E*pm*4cwa;~ zZN=MQ!VLcrBq8-k=dSf8(5Lq!yS@5$8o1)XZvN>%1|F6+@9ukW{gkAkf)bWuiTlWt z&)a(Swwy$}dG^PjMP&6BmEF#XOluJXZ%Q3>I**0lWY)Qr%5*#*-)X2lIqT<>qU6k+ zCi>R6VLt4N5DF=DINdE>OMmZ6pEd@obJ`9~D|x%p;g(-hQ;zA$+4v4=R9UMt-#=!= zuUJ0oR!Nh!xLUk{F_-X|L~bl5F^UHLjx5Y(Em;jD$|$6cd8o#i-HbcY)qnn0v^H$< zRvwQLhe6e^ov@HkTyu^Ht5rard^5NLtWU1<*~L_^Do{{GM^2tW3VM+hNL})ze;bB# z$elTziz6yDvs>rlEdOorqj{spkSQ0gSRyC6v4t>io_WSX`vRcH=vlsT?@l%KIu5z` zT>C{qIA1)s%Mbv)y{N1t!9At{XEN^t`=7f$Bz9oG4&a6Gm2sl8H|pkwU>`k&B(oT0 z!)^~Eo)U9ay!oI!%J(%L(@ow+nLv`6xSefwG9U`AA8-gQ;8Us>_pI&>yEtpw)2#n_ zSo~BYmn?*3lK=sgR6-m~&0?o?023kNk$UJC+G}epolV82Wly|^v}qBVdYJJ%mHnR^ z=l>s5ZyC^J-1dKifJjR>1F1=ebWA`*I+Pw=(%mgc!$eBDLt=E7FuEJ*&e7dG^SbW) ze*Uk{`NbMu~VlrQZWh#q??rAD`*;0z}L}^)4dXVToI5xNYnGqPv>t> zUqkk41b2Zq0cs?--7sl2-2i)x`PxRo-<~n37cO3)sKyQixegU;weRbHNkTDhS%@ zR2=1vlzr4ALb;+dY&mD$|{ZHVATvLy}P zRfIi&ujsJ)W%N-e4_KS0KrsOJgl?mMW%iAS@EsLZvRdWs^-)*O!pavOn@SZp5ZyKRA#g263|9rwD$}--MX;CFwbRemogy;K-+OGAXfh{V%i*b8&;2LUX|P2OCydVA<3j>Q+lY?Pv#oX4Fr z@v`PbH5V|Vs7^fdy+Tqm1m=fa>I1r7^Hw@Qd}w&D(G0X?75Al-$mQ|^&#+}4vw;Tj zu)pAq8~W+c72nT9g8_K6ld1~;fx}u6Y^36{ESbKem=d&sq`SB7C~Fmw4m&aLI zw0g}fM_?co`*zfPpfdY4n%C|bjQbO><(HtdsA4tMHM4rs!Ih(JgXF{^zmBHFz~NK@ ziUgVSDfD}sf0_np?F=K5$U`QdduuO&#-f4BAV8@I>hcpI$E9^ zRITIkpjY~Qp-|xzR~JlH-Ef5?rxcIk8PJoYMK$}&9vAFwQX2|$&D@M7&K`<{J-^wO zPXu8I5L=5j2=n$E`89qfg3<^-{B7d&wcWGBAFPLM4R|j7%sm{O$j-?^w+?nZy4%Tk z1?_Cph;Kn`x#n?c92>CBzA^Or#S%f*WNe!h*F6H!ShYYs>RYmhv5Y>!Ku4un6FHK% z1V(|_=!d%oWpf=Ys|m(?rNEM2S=aAxU>v0BG~g|pR6H)oM0y>J#wyipgSc>PMEWfc zmjUoMi5YvW=qZ)%-e2VX%*u^U&mWhFweVqJ|BEV#H3s_kNJS;k$JVD?=J2`l=01Y` z2{AmSs6v@Pd=;RFC+{pIu>`6!&4Om^9++HHnG4G#j_YLOacW2_Vp# zG4l*l>7=gmtTGb@8tUKO1SuqjK{fx{`OI_g*N%deHZ3Cm3=@ZRkE8G}R=tfVuMe00 ztYsQv;<;Wi$dHaQ7%V$bP->zdJORHP6-#cg>oGA}Fw=~MLsEH{PxaB@^BfVTx_yc$ zI|o&eYG790%kGxbD!(AJRfa9yIPQVVi~HyqEY219>+jZMaf|cJxN65fkJ4&2To3

I!Hdk@EW`lT& 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 z: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 z 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

EB4$M1NfsdP!L? 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�gS(+HHVlSOYQbvLwMF*teiXu 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

vCbj;3EdaxfXCOVI`7O>OBELq+RP2tDG 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