Merge pull request #17260 from tdabasinskas/puppetdb-frontend

Add new puppetdb-plugin
This commit is contained in:
Johan Haals
2023-04-13 14:32:54 +02:00
committed by GitHub
43 changed files with 2206 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+63
View File
@@ -0,0 +1,63 @@
# PuppetDB Plugin
A frontend plugin to integrate PuppetDB with Backstage. When combined with the
[catalog-backend-module-puppetdb](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) plugin, this
frontend plugin allows viewing PuppetDB reports, including their logs and events, of Backstage resource entities.
## Getting started
### Prerequisites
To get started, you need a running instance of PuppetDB. You can find instructions on how to install it
[here](https://www.puppet.com/docs/puppetdb/7/install_via_module.html).
The PuppetDB [should be configured](https://www.puppet.com/docs/puppetdb/7/configure.html#host) to allow being accessed from your Backstage instance.
In addition, your Backstage instance need to either have
[catalog-backend-module-puppetdb](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) plugin installed
or you need to ensure your Resource entities have `puppet.com/certname` annotation set to the PuppetDB node name in some other way.
### Installation
1. Install the plugin with `yarn` in the root of your Backstage application directory:
```bash
yarn --cwd packages/app add @backstage/plugin-puppetdb
```
1. Import and use the plugin in `packages/app/src/App.tsx`:
```tsx
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
const routes = (
<FlatRoutes>
{/* ...other routes */}
<Route path="/puppetdb" element={<PuppetDbPage />} />
</FlatRoutes>
);
```
### Configuration
1. Configure `puppetdb` proxy. As this plugin uses the Backstage proxy to securely communicate with PuppetDB API,
add the following to your `app-config.yaml` to enable this configuration:
```yaml
proxy:
'/puppetdb':
target: https://your.puppetdb.instance.com
```
## Screenshots
#### Main page with the reports list:
![Reports list](https://raw.githubusercontent.com/backstage/backstage/42b65232e763d3e39e2e641b105d2ad469db7a59/plugins/puppetdb/assets/Reports.png)
#### Events for the specific report:
![Events](https://raw.githubusercontent.com/backstage/backstage/1b39e86db17f139dc995f02daca4896533e53eb0/plugins/puppetdb/assets/Events.png)
#### Logs of the specific report:
![Logs](https://raw.githubusercontent.com/backstage/backstage/1b39e86db17f139dc995f02daca4896533e53eb0/plugins/puppetdb/assets/Logs.png)
+35
View File
@@ -0,0 +1,35 @@
## API Report File for "@backstage/plugin-puppetdb"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { PathParams } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
import { SubRouteRef } from '@backstage/core-plugin-api';
// @public
export const isPuppetDbAvailable: (entity: Entity) => boolean;
// @public
export const PuppetDbPage: () => JSX.Element;
// @public
const puppetdbPlugin: BackstagePlugin<{}, {}, {}>;
export { puppetdbPlugin as plugin };
export { puppetdbPlugin };
// @public (undocumented)
export const puppetDbReportRouteRef: SubRouteRef<PathParams<'/:hash'>>;
// @public (undocumented)
export const puppetDbRouteRef: RouteRef<undefined>;
// @public (undocumented)
export const Router: () => JSX.Element;
// (No @packageDocumentation comment for this package)
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { puppetdbPlugin } from '../src';
createDevApp().registerPlugin(puppetdbPlugin).render();
+68
View File
@@ -0,0 +1,68 @@
{
"name": "@backstage/plugin-puppetdb",
"description": "Backstage plugin to visualize resource information and Puppet facts from PuppetDB.",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/puppetdb"
},
"keywords": [
"backstage",
"puppetdb",
"puppet"
],
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"@types/react": "^16.13.1 || ^17.0.0",
"cross-fetch": "^3.1.5",
"msw": "^1.0.1"
},
"files": [
"dist"
]
}
@@ -0,0 +1,206 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UrlPatternDiscovery } from '@backstage/core-app-api';
import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { PuppetDbClient } from './PuppetDbClient';
import {
PuppetDbReport,
PuppetDbReportEvent,
PuppetDbReportLog,
} from './types';
const server = setupServer();
const reports: PuppetDbReport[] = [
{
hash: 'hash1',
puppet_version: 'version1',
report_format: 1,
certname: 'node1',
start_time: '2021-01-01T00:00:00Z',
end_time: '2021-01-01T00:00:00Z',
producer_timestamp: '2021-01-01T00:00:00Z',
receive_time: '2021-01-01T00:00:00Z',
producer: 'producer1',
transaction_uuid: 'uuid1',
catalog_uuid: 'uuid1',
code_id: 'id1',
cached_catalog_status: 'status1',
type: 'type1',
corrective_change: false,
configuration_version: 'version1',
environment: 'production',
noop: false,
status: 'changed',
},
{
hash: 'hash2',
puppet_version: 'version2',
report_format: 2,
certname: 'node1',
start_time: '2021-01-01T00:00:00Z',
end_time: '2021-01-01T00:00:00Z',
producer_timestamp: '2021-01-01T00:00:00Z',
receive_time: '2021-01-01T00:00:00Z',
producer: 'producer2',
transaction_uuid: 'uuid2',
catalog_uuid: 'uuid2',
code_id: 'id2',
cached_catalog_status: 'status2',
type: 'type2',
corrective_change: false,
configuration_version: 'version2',
environment: 'production',
noop: false,
status: 'failed',
},
];
const events: PuppetDbReportEvent[] = [
{
name: 'event1',
message: 'message1',
file: 'file1',
line: 1,
certname: 'node1',
report: 'hash1',
timestamp: '2021-01-01T00:00:00Z',
report_receive_time: '2021-01-01T00:00:00Z',
run_start_time: '2021-01-01T00:00:00Z',
containing_class: 'class1',
environment: 'production',
configuration_version: 'version1',
containment_path: ['path1'],
corrective_change: false,
run_end_time: '2021-01-01T00:00:00Z',
resource_type: 'type1',
resource_title: 'title1',
property: 'property1',
old_value: 'old1',
new_value: 'new1',
status: 'changed',
},
{
name: 'event2',
message: 'message2',
file: 'file2',
line: 2,
certname: 'node1',
report: 'hash1',
timestamp: '2021-01-01T00:00:00Z',
report_receive_time: '2021-01-01T00:00:00Z',
run_start_time: '2021-01-01T00:00:00Z',
containing_class: 'class2',
environment: 'production',
configuration_version: 'version2',
containment_path: ['path2'],
corrective_change: false,
run_end_time: '2021-01-01T00:00:00Z',
resource_type: 'type2',
resource_title: 'title2',
property: 'property2',
old_value: 'old2',
new_value: 'new2',
status: 'failed',
},
];
const logs: PuppetDbReportLog[] = [
{
level: 'level1',
file: 'file1',
line: 1,
time: '2021-01-01T00:00:00Z',
message: 'message1',
source: 'source1',
tags: ['tag1'],
},
{
level: 'level2',
file: 'file2',
line: 2,
time: '2021-01-01T00:00:00Z',
message: 'message2',
source: 'source2',
tags: ['tag2'],
},
];
describe('PuppetDbClient', () => {
setupRequestMockHandlers(server);
const puppetDbCertName = 'node1';
const mockBaseUrl = 'http://backstage:9191/api/proxy';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
let client: PuppetDbClient;
const setupHandlers = () => {
server.use(
rest.get(
`${mockBaseUrl}/puppetdb/pdb/query/v4/reports`,
(req, res, ctx) => {
if (
req.url.searchParams.get('query') !==
`["=","certname","${puppetDbCertName}"]`
) {
return res(ctx.status(400));
}
return res(ctx.status(200), ctx.json(reports));
},
),
rest.get(
`${mockBaseUrl}/puppetdb/pdb/query/v4/reports/hash1/events`,
(_, res, ctx) => {
return res(ctx.status(200), ctx.json(events));
},
),
rest.get(
`${mockBaseUrl}/puppetdb/pdb/query/v4/reports/hash1/logs`,
(_, res, ctx) => {
return res(ctx.status(200), ctx.json(logs));
},
),
);
};
beforeEach(() => {
setupHandlers();
client = new PuppetDbClient({
discoveryApi: discoveryApi,
fetchApi: new MockFetchApi(),
});
});
it('getPuppetDbNodeReports should return reports', async () => {
const got = await client.getPuppetDbNodeReports(puppetDbCertName);
expect(got).toEqual(reports);
});
it('getPuppetDbReportEvents should return events', async () => {
const got = await client.getPuppetDbReportEvents('hash1');
expect(got).toEqual(events);
});
it('getPuppetDbReportLogs should return logs', async () => {
const got = await client.getPuppetDbReportLogs('hash1');
expect(got).toEqual(logs);
});
});
+110
View File
@@ -0,0 +1,110 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
PuppetDbApi,
PuppetDbReport,
PuppetDbReportEvent,
PuppetDbReportLog,
} from './types';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
export class PuppetDbClient implements PuppetDbApi {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
constructor({
discoveryApi,
fetchApi,
}: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}) {
this.discoveryApi = discoveryApi;
this.fetchApi = fetchApi;
}
private async callApi<T>(
path: string,
query: { [key in string]: any },
): Promise<T | undefined> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/puppetdb`;
const response = await this.fetchApi.fetch(
`${apiUrl}${path}?${new URLSearchParams(query).toString()}`,
{
headers: {
'Content-Type': 'application/json',
},
},
);
if (response.ok) {
return (await response.json()) as T;
}
throw await ResponseError.fromResponse(response);
}
async getPuppetDbReportEvents(
puppetDbReportHash: string,
): Promise<PuppetDbReportEvent[] | undefined> {
if (!puppetDbReportHash) {
throw new Error('PuppetDB report hash is required');
}
const events = (await this.callApi(
`/pdb/query/v4/reports/${puppetDbReportHash}/events`,
{},
)) as PuppetDbReportEvent[];
if (!events || events.length === 0) {
return undefined;
}
return events;
}
async getPuppetDbReportLogs(
puppetDbReportHash: string,
): Promise<PuppetDbReportLog[] | undefined> {
if (!puppetDbReportHash) {
throw new Error('PuppetDB report hash is required');
}
const events = (await this.callApi(
`/pdb/query/v4/reports/${puppetDbReportHash}/logs`,
{},
)) as PuppetDbReportLog[];
if (!events || events.length === 0) {
return undefined;
}
return events;
}
async getPuppetDbNodeReports(
puppetDbCertName: string,
): Promise<PuppetDbReport[] | undefined> {
if (!puppetDbCertName) {
throw new Error('PuppetDB certname is required');
}
return this.callApi(`/pdb/query/v4/reports`, {
query: `["=","certname","${puppetDbCertName}"]`,
order_by: `[{"field": "start_time", "order": "desc"},{"field": "end_time", "order": "desc"}]`,
limit: 100,
});
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type {
PuppetDbReport,
PuppetDbReportEvent,
PuppetDbReportLog,
} from './types';
export { puppetDbApiRef } from './types';
export { PuppetDbClient } from './PuppetDbClient';
+198
View File
@@ -0,0 +1,198 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core-plugin-api';
/**
* Report from a PuppetDB.
*/
export type PuppetDbReport = {
/** The name of the node that the report was received from. */
certname: string;
/** The ID of the report. */
hash: string;
/** The environment assigned to the node that submitted the report. */
environment: string;
/** The status associated to report's node. */
status: string;
/** The job id associated with the report. */
job_id?: string;
/** A flag indicating whether the report was produced by a noop run. */
noop: boolean;
/** A flag indicating whether the report contains noop events. */
noop_pending?: boolean;
/** The version of Puppet that generated the report. */
puppet_version: string;
/** The version number of the report format that Puppet used to generate the original report data. */
report_format: number;
/** An identifier string that Puppet uses to match a specific catalog for a node to a specific Puppet run. */
configuration_version: string;
/** The time on the agent at which the Puppet run began. */
start_time: string;
/** The time on the agent at which the Puppet run ended. */
end_time: string;
/** The time of catalog submission from the Puppet Server to PuppetDB. */
producer_timestamp: string;
/** The time at which PuppetDB received the report. */
receive_time: string;
/** The certname of the Puppet Server that sent the report to PuppetDB. */
producer: string;
/** A string used to identify a Puppet run. */
transaction_uuid: string;
/** A string used to tie a catalog to a report to the catalog used from that Puppet run. */
catalog_uuid: string;
/** A string used to tie a catalog to the Puppet code which generated the catalog. */
code_id: string;
/** A string used to identify whether the Puppet run used a cached catalogs. */
cached_catalog_status: string;
/** Either "agent", "plan", or "any" to restrict the results to reports submitted from that source. */
type: string;
/** A flag indicating whether any of the report's events remediated configuration drift. */
corrective_change: boolean;
/** Report metrics. */
metrics?: {
/** Metrics data. */
data: PuppetDbReportMetric[];
/** Link to the metrics endpoint. */
href: string;
};
};
/**
* Resource event generated from Puppet report.
*/
export type PuppetDbReportEvent = {
/** The name of the node on which the event occurred. */
certname: string;
/** The ID of the report that the event occurred in. */
report: string;
/** The status of the event. Legal values are success, failure, noop, and skipped. */
status: string;
/** The timestamp (from the Puppet agent) at which the event occurred. Timestamps are always ISO-8601 compatible date/time strings. */
timestamp: string;
/** The timestamp (from the Puppet agent) at which the Puppet run began. Timestamps are always ISO-8601 compatible date/time strings. */
run_start_time: string;
/** The timestamp (from the Puppet agent) at which the Puppet run finished. Timestamps are always ISO-8601 compatible date/time strings. */
run_end_time: string;
/** The timestamp (from the PuppetDB server) at which the Puppet report was received. Timestamps are always ISO-8601 compatible date/time strings. */
report_receive_time: string;
/** The type of resource that the event occurred on, such as File, Package, etc. */
resource_type: string;
/** The title of the resource on which the event occurred. */
resource_title: string;
/** The property/parameter of the resource on which the event occurred. For example, on a Package resource, this field might have a value of ensure. */
property?: string;
/** The name of the resource on which the event occurred. */
name?: string;
/** The new value that Puppet was attempting to set for the specified resource property. Any rich data values will appear as readable strings. */
new_value?: string;
/** The previous value of the resource property, which Puppet was attempting to change. Any rich data values will appear as readable strings. */
old_value?: string;
/** A description (supplied by the resource provider) of what happened during the event. */
message?: string;
/** The manifest file in which the resource definition is located. */
file?: string;
/** The line (of the containing manifest file) at which the resource definition can be found. */
line?: number;
/** The Puppet class where this resource is declared. */
containing_class?: string;
/** Whether the event occurred in the most recent Puppet run (per-node). */
latest_report?: boolean;
/** The environment associated with the reporting node. */
environment: string;
/** An identifier string that Puppet uses to match a specific catalog for a node to a specific Puppet run. */
configuration_version: string;
/** The containment path associated with the event, as an ordered array that ends with the most specific containing element. */
containment_path: string[];
/** Whether the event represents a "corrective change", meaning the event rectified configuration drift. */
corrective_change: boolean;
};
/**
* Resource log generated from Puppet report.
*/
export type PuppetDbReportLog = {
/** The manifest file in which the resource definition is located. */
file?: string;
/** The line (of the containing manifest file) at which the resource definition can be found. */
line?: number;
/** The timestamp (from the Puppet agent) at which the event occurred. Timestamps are always ISO-8601 compatible date/time strings. */
time: string;
/** A description (supplied by the resource provider) of what happened during the event. */
message?: string;
/** The log level. */
level: string;
/** Log source */
source: string;
/** Resource tags */
tags: string[];
};
/**
* A metric from a PuppetDB report.
*/
export type PuppetDbReportMetric = {
/**
* The name of the metric.
*/
name: string;
/**
* The value of the metric.
*/
value: string;
/**
* The category of the metric.
*/
category: string;
};
export const puppetDbApiRef = createApiRef<PuppetDbApi>({
id: 'plugin.puppetdb.service',
});
/**
* The API provided by the PuppetDB plugin.
*/
export type PuppetDbApi = {
/**
* Get a list of PuppetDB reports for the specified node.
*
* @param puppetDbCertName - The name of the node that the report was received from.
* @returns A list of PuppetDB reports for the specified node.
*/
getPuppetDbNodeReports(
puppetDbCertName: string,
): Promise<PuppetDbReport[] | undefined>;
/**
* Get a specific PuppetDB report events.
*
* @param puppetDbReportHash - The ID of the report.
* @returns Events from a specific PuppetDB report.
*/
getPuppetDbReportEvents(
puppetDbReportHash: string,
): Promise<PuppetDbReportEvent[] | undefined>;
/**
* Get a specific PuppetDB report logs.
*
* @param puppetDbReportHash - The ID of the report.
* @returns Logs from a specific PuppetDB report.
*/
getPuppetDbReportLogs(
puppetDbReportHash: string,
): Promise<PuppetDbReportLog[] | undefined>;
};
@@ -0,0 +1,105 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ReportDetailsEventsTable } from './ReportDetailsEventsTable';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { puppetDbApiRef, PuppetDbReportEvent } from '../../api';
import { PuppetDbClient } from '../../api';
import { ApiProvider } from '@backstage/core-app-api';
import { puppetDbRouteRef } from '../../routes';
const events: PuppetDbReportEvent[] = [
{
name: 'event3',
message: 'message1',
file: 'file1',
line: 1,
certname: 'node1',
report: 'hash1',
timestamp: '2021-01-01T00:00:00Z',
report_receive_time: '2021-01-01T00:00:00Z',
run_start_time: '2021-01-01T00:00:00Z',
containing_class: 'class1',
environment: 'production',
configuration_version: 'version1',
containment_path: ['path1'],
corrective_change: false,
run_end_time: '2021-01-01T00:00:00Z',
resource_type: 'type1',
resource_title: 'title1',
property: 'property1',
old_value: 'old1',
new_value: 'new1',
status: 'changed',
},
{
name: 'event2',
message: 'message2',
file: 'file2',
line: 2,
certname: 'node1',
report: 'hash1',
timestamp: '2021-01-01T00:00:00Z',
report_receive_time: '2021-01-01T00:00:00Z',
run_start_time: '2021-01-01T00:00:00Z',
containing_class: 'class2',
environment: 'production',
configuration_version: 'version2',
containment_path: ['path2'],
corrective_change: false,
run_end_time: '2021-01-01T00:00:00Z',
resource_type: 'type2',
resource_title: 'title2',
property: 'property2',
old_value: 'old2',
new_value: 'new2',
status: 'failed',
},
];
const mockPuppetDbApi: Partial<PuppetDbClient> = {
getPuppetDbReportEvents: async () => events,
};
const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]);
const hash = 'hash';
describe('ReportEventsTable', () => {
it('should render', async () => {
const render = await renderInTestApp(
<ApiProvider apis={apis}>
<ReportDetailsEventsTable hash={hash} />
</ApiProvider>,
{
mountedRoutes: {
'/puppetdb/report/:hash': puppetDbRouteRef,
},
},
);
expect(render.getByText('Latest events')).toBeInTheDocument();
expect(render.getByText('Containing Class')).toBeInTheDocument();
expect(render.getByText('Resource')).toBeInTheDocument();
expect(render.getByText('Property')).toBeInTheDocument();
expect(render.getByText('Old Value')).toBeInTheDocument();
expect(render.getByText('New Value')).toBeInTheDocument();
expect(render.getByText('Status')).toBeInTheDocument();
expect(render.getByText('property2')).toBeInTheDocument();
expect(render.getByText('old1')).toBeInTheDocument();
expect(render.getByText('old2')).toBeInTheDocument();
});
});
@@ -0,0 +1,145 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Typography from '@material-ui/core/Typography';
import React from 'react';
import { puppetDbApiRef, PuppetDbReportEvent } from '../../api';
import {
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import { makeStyles } from '@material-ui/core/styles';
import { StatusField } from '../StatusField';
type ReportEventsTableProps = {
hash: string;
};
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
/**
* Component for displaying PuppetDB report events.
*
* @public
*/
export const ReportDetailsEventsTable = (props: ReportEventsTableProps) => {
const { hash } = props;
const puppetDbApi = useApi(puppetDbApiRef);
const classes = useStyles();
const { value, loading, error } = useAsync(async () => {
return puppetDbApi.getPuppetDbReportEvents(hash);
}, [puppetDbApi, hash]);
if (error) {
return <ResponseErrorPanel error={error} />;
}
const columns: TableColumn<PuppetDbReportEvent>[] = [
{
title: 'Run Start Time',
field: 'run_start_time',
align: 'center',
width: '300px',
render: rowData => (
<Typography noWrap>
{new Date(Date.parse(rowData.run_start_time)).toLocaleString()}
</Typography>
),
},
{
title: 'Run End Time',
field: 'run_end_time',
align: 'center',
width: '300px',
render: rowData => (
<Typography noWrap>
{new Date(Date.parse(rowData.run_end_time)).toLocaleString()}
</Typography>
),
},
{
title: 'Containing Class',
field: 'containing_class',
render: rowData => (
<Typography noWrap title={rowData.file || ''}>
{rowData.containing_class}
</Typography>
),
},
{
title: 'Resource',
field: 'resource_title',
render: rowData => (
<Typography noWrap>
{rowData.resource_type}[{rowData.resource_title}]
</Typography>
),
},
{
title: 'Property',
field: 'property',
render: rowData => <Typography noWrap>{rowData.property}</Typography>,
},
{
title: 'Old Value',
field: 'old_value',
render: rowData => <Typography noWrap>{rowData.old_value}</Typography>,
},
{
title: 'New Value',
field: 'new_value',
render: rowData => <Typography noWrap>{rowData.new_value}</Typography>,
},
{
title: 'Status',
field: 'status',
render: rowData => <StatusField status={rowData.status} />,
},
];
return (
<Table
options={{
sorting: true,
actionsColumnIndex: -1,
loadingType: 'linear',
padding: 'dense',
showEmptyDataSourceMessage: !loading,
showTitle: true,
toolbar: true,
pageSize: 10,
pageSizeOptions: [10],
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No events
</Typography>
}
title="Latest events"
columns={columns}
data={value || []}
isLoading={loading}
/>
);
};
@@ -0,0 +1,74 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ReportDetailsLogsTable } from './ReportDetailsLogsTable';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { puppetDbApiRef, PuppetDbReportLog } from '../../api';
import { PuppetDbClient } from '../../api';
import { ApiProvider } from '@backstage/core-app-api';
import { puppetDbRouteRef } from '../../routes';
const logs: PuppetDbReportLog[] = [
{
level: 'level1',
file: 'file1',
line: 1,
time: '2021-01-01T00:00:00Z',
message: 'message1',
source: 'source1',
tags: ['tag1'],
},
{
level: 'level2',
file: 'file2',
line: 2,
time: '2021-01-01T00:00:00Z',
message: 'message2',
source: 'source2',
tags: ['tag2'],
},
];
const mockPuppetDbApi: Partial<PuppetDbClient> = {
getPuppetDbReportLogs: async () => logs,
};
const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]);
const hash = 'hash';
describe('ReportLogsTable', () => {
it('should render', async () => {
const render = await renderInTestApp(
<ApiProvider apis={apis}>
<ReportDetailsLogsTable hash={hash} />
</ApiProvider>,
{
mountedRoutes: {
'/puppetdb/report/:hash': puppetDbRouteRef,
},
},
);
expect(render.getByText('Latest logs')).toBeInTheDocument();
expect(render.getByText('Level')).toBeInTheDocument();
expect(render.getByText('Timestamp')).toBeInTheDocument();
expect(render.getByText('Source')).toBeInTheDocument();
expect(render.getByText('Message')).toBeInTheDocument();
expect(render.getByText('source1')).toBeInTheDocument();
expect(render.getByText('source2')).toBeInTheDocument();
});
});
@@ -0,0 +1,133 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Typography from '@material-ui/core/Typography';
import React from 'react';
import { puppetDbApiRef, PuppetDbReportLog } from '../../api';
import {
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import { makeStyles } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
type ReportLogsTableProps = {
hash: string;
};
const useStyles = makeStyles<BackstageTheme>(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
level_error: {
color: theme.palette.error.light,
},
level_warning: {
color: theme.palette.warning.light,
},
level_notice: {
color: theme.palette.info.light,
},
}));
/**
* Component for displaying PuppetDB report logs.
*
* @public
*/
export const ReportDetailsLogsTable = (props: ReportLogsTableProps) => {
const { hash } = props;
const puppetDbApi = useApi(puppetDbApiRef);
const classes = useStyles();
const { value, loading, error } = useAsync(async () => {
return puppetDbApi.getPuppetDbReportLogs(hash);
}, [puppetDbApi, hash]);
if (error) {
return <ResponseErrorPanel error={error} />;
}
const columns: TableColumn<PuppetDbReportLog>[] = [
{
title: 'Level',
field: 'level',
align: 'center',
width: '100px',
render: rowData => (
<Typography
noWrap
className={
(rowData.level === 'warning' && classes.level_warning) ||
(rowData.level === 'error' && classes.level_error) ||
classes.level_notice
}
>
{rowData.level.toLocaleUpperCase('en-US')}
</Typography>
),
},
{
title: 'Timestamp',
field: 'time',
align: 'center',
width: '300px',
render: rowData => (
<Typography noWrap>
{new Date(Date.parse(rowData.time)).toLocaleString()}
</Typography>
),
},
{
title: 'Source',
field: 'source',
render: rowData => <Typography noWrap>{rowData.source}</Typography>,
},
{
title: 'Message',
field: 'message',
render: rowData => <Typography noWrap>{rowData.message}</Typography>,
},
];
return (
<Table
options={{
sorting: true,
actionsColumnIndex: -1,
loadingType: 'linear',
padding: 'dense',
showEmptyDataSourceMessage: !loading,
showTitle: true,
toolbar: true,
pageSize: 10,
pageSizeOptions: [10],
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No logs
</Typography>
}
title="Latest logs"
columns={columns}
data={value || []}
isLoading={loading}
/>
);
};
@@ -0,0 +1,45 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ReportDetailsPage } from './ReportDetailsPage';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { puppetDbRouteRef } from '../../routes';
import { puppetDbApiRef, PuppetDbClient } from '../../api';
import { ApiProvider } from '@backstage/core-app-api';
const mockPuppetDbApi: Partial<PuppetDbClient> = {
getPuppetDbReportEvents: async () => [],
};
const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]);
describe('ReportDetailsPage', () => {
it('should render', async () => {
const render = await renderInTestApp(
<ApiProvider apis={apis}>
<ReportDetailsPage />
</ApiProvider>,
{
mountedRoutes: {
'/puppetdb/report': puppetDbRouteRef,
},
},
);
expect(render.getByText('PuppetDB Reports')).toBeInTheDocument();
expect(render.getByText('Events')).toBeInTheDocument();
expect(render.getByText('Logs')).toBeInTheDocument();
});
});
@@ -0,0 +1,106 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Link as RouterLink, useParams } from 'react-router-dom';
import { Breadcrumbs, Link } from '@backstage/core-components';
import { makeStyles } from '@material-ui/core/styles';
import { useRouteRef } from '@backstage/core-plugin-api';
import { puppetDbRouteRef } from '../../routes';
import React, { useState } from 'react';
import {
Card,
CardContent,
Tab,
Box,
Typography,
Tabs,
} from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { ReportDetailsEventsTable } from './ReportDetailsEventsTable';
import { ReportDetailsLogsTable } from './ReportDetailsLogsTable';
const useStyles = makeStyles<BackstageTheme>(theme => ({
cards: {
marginTop: theme.spacing(2),
},
tabs: {
borderBottom: `1px solid ${theme.palette.textVerySubtle}`,
backgroundColor: theme.palette.background.default,
padding: theme.spacing(0, 4),
},
default: {
padding: theme.spacing(2),
fontWeight: theme.typography.fontWeightBold,
color: theme.palette.text.secondary,
textTransform: 'uppercase',
},
selected: {
color: theme.palette.text.primary,
},
}));
/**
* Component for displaying the details of a PuppetDB report.
*
* @public
*/
export const ReportDetailsPage = () => {
const { hash = '' } = useParams();
const classes = useStyles();
const [tabIndex, setTabIndex] = useState(0);
const reportsRouteLink = useRouteRef(puppetDbRouteRef);
const tabs = [
{ id: 'events', label: 'Events' },
{ id: 'logs', label: 'Logs' },
];
const safeTabIndex = tabIndex > tabs.length - 1 ? 0 : tabIndex;
return (
<div>
<Breadcrumbs aria-label="breadcrumb">
<Link component={RouterLink} to={reportsRouteLink()}>
PuppetDB Reports
</Link>
<Typography noWrap>{hash}</Typography>
</Breadcrumbs>
<Card
style={{ position: 'relative', overflow: 'visible' }}
className={classes.cards}
>
<CardContent>
<Tabs
indicatorColor="primary"
onChange={(_, index) => setTabIndex(index)}
value={safeTabIndex}
>
{tabs.map((tab, index) => (
<Tab
className={classes.default}
label={tab.label}
key={tab.id}
value={index}
classes={{ selected: classes.selected }}
/>
))}
</Tabs>
<Box ml={2} pt={2} my={1} display="flex" flexDirection="column">
{safeTabIndex === 0 && <ReportDetailsEventsTable hash={hash} />}
{safeTabIndex === 1 && <ReportDetailsLogsTable hash={hash} />}
</Box>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ReportDetailsPage } from './ReportDetailsPage';
export { ReportDetailsEventsTable } from './ReportDetailsEventsTable';
export { ReportDetailsLogsTable } from './ReportDetailsLogsTable';
@@ -0,0 +1,85 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ReportsPage } from './ReportsPage';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { puppetDbApiRef, PuppetDbClient } from '../../api';
import { ANNOTATION_PUPPET_CERTNAME } from '../../constants';
import { Entity } from '@backstage/catalog-model/';
import { isPuppetDbAvailable } from '../Router';
import { ApiProvider } from '@backstage/core-app-api';
import { puppetDbRouteRef } from '../../routes';
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Resource',
metadata: {
name: 'test',
annotations: {
[ANNOTATION_PUPPET_CERTNAME]: 'node1',
},
},
};
const entityWithoutAnnotations: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Resource',
metadata: {
name: 'test',
annotations: {},
},
};
const mockPuppetDbApi: Partial<PuppetDbClient> = {
getPuppetDbNodeReports: async () => [],
};
const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]);
describe('isPuppetDbAvailable', () => {
describe('when entity has no annotations', () => {
it('returns false', () => {
expect(isPuppetDbAvailable(entityWithoutAnnotations)).toBe(false);
});
});
describe('when entity has the puppet annotation', () => {
it('returns true', () => {
expect(isPuppetDbAvailable(entity)).toBe(true);
});
});
});
describe('ReportsPage', () => {
it('should render', async () => {
const render = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<ReportsPage />
</EntityProvider>
</ApiProvider>,
{
mountedRoutes: {
'/puppetdb': puppetDbRouteRef,
},
},
);
expect(
render.getByText('Latest PuppetDB reports from node node1'),
).toBeInTheDocument();
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ReportsTable } from './ReportsTable';
import { Page, Content } from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { ANNOTATION_PUPPET_CERTNAME } from '../../constants';
/**
* Component to display PuppetDB reports page.
*
* @public
*/
export const ReportsPage = () => {
const { entity } = useEntity();
const certName =
entity?.metadata?.annotations?.[ANNOTATION_PUPPET_CERTNAME] ?? '';
return (
<Page themeId="tool">
<Content>
<ReportsTable certName={certName} />
</Content>
</Page>
);
};
@@ -0,0 +1,103 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ReportsTable } from './ReportsTable';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { puppetDbApiRef, PuppetDbReport, PuppetDbClient } from '../../api';
import { ApiProvider } from '@backstage/core-app-api';
import { puppetDbRouteRef } from '../../routes';
const reports: PuppetDbReport[] = [
{
hash: 'hash1',
puppet_version: 'version1',
report_format: 1,
certname: 'node1',
start_time: '2021-01-01T00:00:00Z',
end_time: '2021-01-01T00:00:00Z',
producer_timestamp: '2021-01-01T00:00:00Z',
receive_time: '2021-01-01T00:00:00Z',
producer: 'producer1',
transaction_uuid: 'uuid1',
catalog_uuid: 'uuid1',
code_id: 'id1',
cached_catalog_status: 'status1',
type: 'type1',
corrective_change: false,
configuration_version: 'version1',
environment: 'production',
noop: true,
status: 'changed',
},
{
hash: 'hash2',
puppet_version: 'version2',
report_format: 2,
certname: 'node1',
start_time: '2021-01-01T00:00:00Z',
end_time: '2021-01-01T00:00:00Z',
producer_timestamp: '2021-01-01T00:00:00Z',
receive_time: '2021-01-01T00:00:00Z',
producer: 'producer2',
transaction_uuid: 'uuid2',
catalog_uuid: 'uuid2',
code_id: 'id2',
cached_catalog_status: 'status2',
type: 'type2',
corrective_change: false,
configuration_version: 'version2',
environment: 'production',
noop: false,
status: 'failed',
},
];
const mockPuppetDbApi: Partial<PuppetDbClient> = {
getPuppetDbNodeReports: async () => reports,
};
const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]);
const certName = 'node1';
describe('ReportsTable', () => {
it('should render', async () => {
const render = await renderInTestApp(
<ApiProvider apis={apis}>
<ReportsTable certName={certName} />
</ApiProvider>,
{
mountedRoutes: {
'/puppetdb': puppetDbRouteRef,
},
},
);
expect(
render.getByText('Latest PuppetDB reports from node node1'),
).toBeInTheDocument();
expect(render.getByText('Configuration Version')).toBeInTheDocument();
expect(render.getByText('Start Time')).toBeInTheDocument();
expect(render.getByText('End Time')).toBeInTheDocument();
expect(render.getByText('Run Duration')).toBeInTheDocument();
expect(render.getByText('Mode')).toBeInTheDocument();
expect(render.getByText('Status')).toBeInTheDocument();
expect(render.getByText('NO-NOOP')).toBeInTheDocument();
expect(render.getByText('NOOP')).toBeInTheDocument();
expect(render.getByText('FAILED')).toBeInTheDocument();
expect(render.getByText('CHANGED')).toBeInTheDocument();
});
});
@@ -0,0 +1,169 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { puppetDbApiRef, PuppetDbReport } from '../../api';
import useAsync from 'react-use/lib/useAsync';
import React from 'react';
import {
Link,
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import { Link as RouterLink } from 'react-router-dom';
import { puppetDbReportRouteRef } from '../../routes';
import { StatusField } from '../StatusField';
type ReportsTableProps = {
certName: string;
};
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
/**
* Component for displaying a table of PuppetDB reports for a given node.
*
* @public
*/
export const ReportsTable = (props: ReportsTableProps) => {
const { certName } = props;
const puppetDbApi = useApi(puppetDbApiRef);
const reportsRouteLink = useRouteRef(puppetDbReportRouteRef);
const classes = useStyles();
const { value, loading, error } = useAsync(async () => {
return puppetDbApi.getPuppetDbNodeReports(certName);
}, [puppetDbApi, certName]);
if (error) {
return <ResponseErrorPanel error={error} />;
}
const columns: TableColumn<PuppetDbReport>[] = [
{
title: 'Configuration Version',
field: 'configuration_version',
render: rowData => (
<Link
component={RouterLink}
to={reportsRouteLink({ hash: rowData.hash! })}
>
{rowData.configuration_version !== '' ? (
<Typography noWrap>{rowData.configuration_version}</Typography>
) : (
<Typography noWrap>
<em>(N/A)</em>
</Typography>
)}
</Link>
),
},
{
title: 'Start Time',
field: 'start_time',
align: 'center',
width: '300px',
render: rowData => (
<Typography noWrap>
{new Date(Date.parse(rowData.start_time)).toLocaleString()}
</Typography>
),
},
{
title: 'End Time',
field: 'end_time',
align: 'center',
width: '300px',
render: rowData => (
<Typography noWrap>
{new Date(Date.parse(rowData.end_time)).toLocaleString()}
</Typography>
),
},
{
title: 'Run Duration',
align: 'center',
width: '400px',
render: rowData => {
const start_date = new Date(Date.parse(rowData.start_time));
const end_date = new Date(Date.parse(rowData.end_time));
const duration = new Date(end_date.getTime() - start_date.getTime());
return (
<Typography noWrap>
{duration.getUTCHours().toString().padStart(2, '0')}:
{duration.getUTCMinutes().toString().padStart(2, '0')}:
{duration.getUTCSeconds().toString().padStart(2, '0')}.
{duration.getUTCMilliseconds().toString().padStart(4, '0')}
</Typography>
);
},
},
{
title: 'Environment',
field: 'environment',
},
{
title: 'Mode',
field: 'noop',
align: 'center',
render: rowData =>
rowData.noop ? (
<Typography>NOOP</Typography>
) : (
<Typography>NO-NOOP</Typography>
),
},
{
title: 'Status',
field: 'status',
align: 'center',
render: rowData => <StatusField status={rowData.status} />,
},
];
return (
<Table
options={{
sorting: true,
actionsColumnIndex: -1,
loadingType: 'linear',
padding: 'dense',
showEmptyDataSourceMessage: !loading,
showTitle: true,
toolbar: true,
pageSize: 10,
pageSizeOptions: [10],
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No reports
</Typography>
}
title={`Latest PuppetDB reports from node ${certName}`}
columns={columns}
data={value || []}
isLoading={loading}
/>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ReportsPage } from './ReportsPage';
export { ReportsTable } from './ReportsTable';
@@ -0,0 +1,55 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { puppetDbReportRouteRef } from '../routes';
import { ANNOTATION_PUPPET_CERTNAME } from '../constants';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { MissingAnnotationEmptyState } from '@backstage/core-components';
import { ReportsPage } from './ReportsPage';
import { ReportDetailsPage } from './ReportDetailsPage';
/**
* Checks if the entity has a puppet certname annotation.
* @param entity - The entity to check for the puppet certname annotation.
*
* @public
*/
export const isPuppetDbAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[ANNOTATION_PUPPET_CERTNAME]);
/** @public */
export const Router = () => {
const { entity } = useEntity();
if (!isPuppetDbAvailable(entity)) {
return (
<MissingAnnotationEmptyState annotation={ANNOTATION_PUPPET_CERTNAME} />
);
}
return (
<Routes>
<Route path="/" element={<ReportsPage />} />
<Route
path={`${puppetDbReportRouteRef.path}`}
element={<ReportDetailsPage />}
/>
</Routes>
);
};
@@ -0,0 +1,52 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { StatusField } from './StatusField';
import { renderInTestApp } from '@backstage/test-utils';
describe('StatusField', () => {
it('should render failed', async () => {
const render = await renderInTestApp(<StatusField status="failed" />);
expect(render.getByText('FAILED')).toBeInTheDocument();
expect(
render.container.querySelector('span[aria-label="Status error"]'),
).toBeInTheDocument();
});
it('should render changed', async () => {
const render = await renderInTestApp(<StatusField status="changed" />);
expect(render.getByText('CHANGED')).toBeInTheDocument();
expect(
render.container.querySelector('span[aria-label="Status running"]'),
).toBeInTheDocument();
});
it('should render unchanged', async () => {
const render = await renderInTestApp(<StatusField status="unchanged" />);
expect(render.getByText('UNCHANGED')).toBeInTheDocument();
expect(
render.container.querySelector('span[aria-label="Status pending"]'),
).toBeInTheDocument();
});
it('should render default', async () => {
const render = await renderInTestApp(<StatusField status="successful" />);
expect(render.getByText('SUCCESSFUL')).toBeInTheDocument();
expect(
render.container.querySelector('span[aria-label="Status ok"]'),
).toBeInTheDocument();
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
StatusPending,
StatusRunning,
StatusOK,
StatusError,
} from '@backstage/core-components';
type StatusCellProps = {
status: string;
};
/**
* Component to display status field for Puppet reports and events.
*
* @public
*/
export const StatusField = (props: StatusCellProps) => {
const { status } = props;
const statusUC = status.toLocaleUpperCase('en-US');
switch (status) {
case 'failed':
return (
<>
<StatusError />
{statusUC}
</>
);
case 'changed':
return (
<>
<StatusRunning />
{statusUC}
</>
);
case 'unchanged':
return (
<>
<StatusPending />
{statusUC}
</>
);
default:
return (
<>
<StatusOK />
{statusUC}
</>
);
}
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { StatusField } from './StatusField';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Annotation for specifying the certificate name of a node in PuppetDB.
*
* @public
*/
export const ANNOTATION_PUPPET_CERTNAME = 'puppet.com/certname';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
puppetdbPlugin,
puppetdbPlugin as plugin,
PuppetDbPage,
} from './plugin';
export { Router, isPuppetDbAvailable } from './components/Router';
export * from './routes';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { puppetdbPlugin } from './plugin';
describe('puppetdb', () => {
it('should export plugin', () => {
expect(puppetdbPlugin).toBeDefined();
});
});
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { puppetDbApiRef, PuppetDbClient } from './api';
import { puppetDbRouteRef } from './routes';
/**
* Create the PuppetDB frontend plugin.
*
* @public
* */
export const puppetdbPlugin = createPlugin({
id: 'puppetDb',
apis: [
createApiFactory({
api: puppetDbApiRef,
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) =>
new PuppetDbClient({ discoveryApi, fetchApi }),
}),
],
});
/**
* Creates a routable extension for the PuppetDB plugin content.
*
* @public
*/
export const PuppetDbPage = puppetdbPlugin.provide(
createRoutableExtension({
name: 'PuppetDbPage',
component: () => import('./components/Router').then(m => m.Router),
mountPoint: puppetDbRouteRef,
}),
);
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
/** @public */
export const puppetDbRouteRef = createRouteRef({
id: 'puppetdb',
});
/** @public */
export const puppetDbReportRouteRef = createSubRouteRef({
id: 'puppetdb/report',
path: '/:hash',
parent: puppetDbRouteRef,
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';