Add reports list

Signed-off-by: Tomas Dabasinskas <tomas@dabasinskas.net>
This commit is contained in:
Tomas Dabasinskas
2023-02-27 07:52:10 +02:00
parent c4a9a16af2
commit 8bd70c7ae6
11 changed files with 471 additions and 19 deletions
+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();
+4 -3
View File
@@ -44,7 +44,9 @@
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
"react": "^16.13.1 || ^17.0.0",
"@types/react": "^16.13.1 || ^17.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
@@ -60,6 +62,5 @@
},
"files": [
"dist"
],
"configSchema": "config.d.ts"
]
}
+34 -2
View File
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PuppetDbApi, PuppetDbNode } from './types';
import { PuppetDbApi, PuppetDbNode, PuppetDbReport } from './types';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
@@ -38,7 +38,7 @@ export class PuppetDbClient implements PuppetDbApi {
): Promise<T | undefined> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/puppetdb`;
const response = await this.fetchApi.fetch(
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
`${apiUrl}${path}?${new URLSearchParams(query).toString()}`,
{
headers: {
'Content-Type': 'application/json',
@@ -63,4 +63,36 @@ export class PuppetDbClient implements PuppetDbApi {
{},
);
}
async getPuppetDbReport(
puppetDbReportHash: string,
): Promise<PuppetDbReport | undefined> {
if (!puppetDbReportHash) {
throw new Error('PuppetDB report hash is required');
}
const reports = (await this.callApi(`/pdb/query/v4/reports`, {
query: `["=","hash","${puppetDbReportHash}"]`,
})) as PuppetDbReport[];
if (!reports || reports.length === 0) {
return undefined;
}
return reports[0];
}
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: 30,
});
}
}
+1 -1
View File
@@ -13,6 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { PuppetDbNode } from './types';
export type { PuppetDbNode, PuppetDbReport } from './types';
export { puppetDbApiRef } from './types';
export { PuppetDbClient } from './PuppetDbClient';
+147
View File
@@ -85,10 +85,157 @@ export type PuppetDbNode = {
expired?: boolean;
};
/**
* 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;
};
};
/**
* 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 = {
/**
*
* @param puppetDbCertName - The name of the PuppetDB node.
* @returns A PuppetDB node.
*/
getPuppetDbNode(puppetDbCertName: string): Promise<PuppetDbNode | undefined>;
/**
* 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.
*
* @param puppetDbReportHash - The ID of the report.
* @returns A specific PuppetDB report.
*/
getPuppetDbReport(
puppetDbReportHash: string,
): Promise<PuppetDbReport | undefined>;
};
@@ -18,18 +18,18 @@ import useAsync from 'react-use/lib/useAsync';
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
import { InfoCard } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { puppetDbApiRef } from '../../api';
import { puppetDbApiRef } from '../../../api';
type PuppetDbNodeCardProps = {
certName: string;
};
export const PuppetDbNodeCard = (props: PuppetDbNodeCardProps) => {
export const NodeCard = (props: PuppetDbNodeCardProps) => {
const { certName } = props;
const puppetDbApi = useApi(puppetDbApiRef);
const { value, loading, error } = useAsync(async () => {
return puppetDbApi.getPuppetDbNode(certName);
return puppetDbApi.getPuppetDbReport(certName);
}, [puppetDbApi, certName]);
if (loading) {
@@ -39,11 +39,8 @@ export const PuppetDbNodeCard = (props: PuppetDbNodeCardProps) => {
}
return (
<InfoCard
title="PuppetDB Node"
subheader={`Details about PuppetDB node ${certName}`}
>
<text>${JSON.stringify(value)}</text>
<InfoCard title={`Puppet node: ${certName}`}>
<pre>{JSON.stringify(value, null, 2)}</pre>
</InfoCard>
);
};
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { PuppetDbNodeCard } from './PuppetDbNodeCard';
export { NodeCard } from './NodeCard/NodeCard';
@@ -22,7 +22,8 @@ import {
import { useEntity } from '@backstage/plugin-catalog-react';
import { isPuppetDbAvailable } from '../../plugin';
import { ANNOTATION_PUPPET_CERTNAME } from '../../constants';
import { PuppetDbNodeCard } from '../PuppetDbNodeCard';
import { ReportsTable } from '../Reports/ReportsTable/ReportsTable';
import { NodeCard } from '../Node';
export const PuppetDbTab = () => {
const { entity } = useEntity();
@@ -34,12 +35,13 @@ export const PuppetDbTab = () => {
}
// @ts-ignore
// TODO(tdabasinskas): Remove the static value after the testing.
const certName = entity.metadata.annotations[ANNOTATION_PUPPET_CERTNAME];
return (
<Page themeId="tool">
<Content>
<PuppetDbNodeCard certName={certName} />
<ReportsTable certName={certName} />
</Content>
</Page>
);
@@ -0,0 +1,248 @@
/*
* 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,
StatusPending,
StatusRunning,
StatusOK,
StatusAborted,
StatusError,
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 { buildRouteRef } from '../../../routes';
type ReportsTableProps = {
certName: string;
};
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const ReportsTable = (props: ReportsTableProps) => {
const { certName } = props;
const puppetDbApi = useApi(puppetDbApiRef);
const routeLink = useRouteRef(buildRouteRef);
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={routeLink({ hash: rowData.hash! })}>
<Typography noWrap>{rowData.configuration_version}</Typography>
</Link>
),
},
{
title: 'Start Time',
field: 'start_time',
align: 'center',
render: rowData => (
<Typography>
{new Date(Date.parse(rowData.start_time)).toLocaleString()}
</Typography>
),
},
{
title: 'End Time',
field: 'end_time',
align: 'center',
render: rowData => (
<Typography>
{new Date(Date.parse(rowData.end_time)).toLocaleString()}
</Typography>
),
},
{
title: 'Run Duration',
align: 'center',
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 => {
if (rowData.noop) {
return (
<>
<StatusAborted />
{rowData.status.charAt(0).toLocaleUpperCase() +
rowData.status.slice(1)}
</>
);
}
switch (rowData.status) {
case 'failed':
return (
<>
<StatusError />
{rowData.status.charAt(0).toLocaleUpperCase() +
rowData.status.slice(1)}
</>
);
case 'changed':
return (
<>
<StatusRunning />
{rowData.status.charAt(0).toLocaleUpperCase() +
rowData.status.slice(1)}
</>
);
case 'unchanged':
return (
<>
<StatusPending />
{rowData.status.charAt(0).toLocaleUpperCase() +
rowData.status.slice(1)}
</>
);
default:
return (
<>
<StatusOK />
{rowData.status.charAt(0).toLocaleUpperCase() +
rowData.status.slice(1)}
</>
);
}
},
},
];
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}
/>
);
/*
return (
<TableContainer className={classes.table}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Start Time</TableCell>
<TableCell>End Time</TableCell>
<TableCell>Duration</TableCell>
<TableCell>Environment</TableCell>
<TableCell>Configuration Version</TableCell>
<TableCell>Puppet Version</TableCell>
<TableCell>NoOp</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{value?.map((report) => {
const start_date = new Date(Date.parse(report.start_time));
const end_date = new Date(Date.parse(report.end_time));
const duration = new Date(end_date.getTime() - start_date.getTime());
return (<TableRow>
<TableCell>{start_date.toLocaleString()}</TableCell>
<TableCell>{end_date.toLocaleString()}</TableCell>
<TableCell>
{duration.getUTCHours().toString().padStart(2, '0')}:
{duration.getUTCMinutes().toString().padStart(2, '0')}:
{duration.getUTCSeconds().toString().padStart(2, '0')}.
{duration.getUTCMilliseconds().toString().padStart(4, '0')}
</TableCell>
<TableCell>{report.environment}</TableCell>
<TableCell>{report.configuration_version}</TableCell>
<TableCell>{report.puppet_version}</TableCell>
<TableCell>{report.noop ? 'Yes' : 'No'}</TableCell>
<TableCell>{report.status}</TableCell>
</TableRow>
)}
)}
</TableBody>
</Table>
</TableContainer>
);*/
};
+7 -1
View File
@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'puppetdb',
});
export const buildRouteRef = createSubRouteRef({
id: 'puppetdb/report',
path: '/:hash',
parent: rootRouteRef,
});