diff --git a/app-config.yaml b/app-config.yaml index e2b03786c4..e0e962cb50 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -124,7 +124,7 @@ proxy: St2-Api-Key: ${ST2_API_KEY} '/puppetdb': - target: https://your.puppetdb.instance.com + #target: https://your.puppetdb.instance.com organization: name: My Company diff --git a/plugins/puppetdb/dev/index.tsx b/plugins/puppetdb/dev/index.tsx new file mode 100644 index 0000000000..8082afcfe2 --- /dev/null +++ b/plugins/puppetdb/dev/index.tsx @@ -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(); diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json index c731998d1f..bd143958e7 100644 --- a/plugins/puppetdb/package.json +++ b/plugins/puppetdb/package.json @@ -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" + ] } diff --git a/plugins/puppetdb/src/api/PuppetDbClient.ts b/plugins/puppetdb/src/api/PuppetDbClient.ts index f1653613e5..aecca48118 100644 --- a/plugins/puppetdb/src/api/PuppetDbClient.ts +++ b/plugins/puppetdb/src/api/PuppetDbClient.ts @@ -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 { 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 { + 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 { + 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, + }); + } } diff --git a/plugins/puppetdb/src/api/index.ts b/plugins/puppetdb/src/api/index.ts index 905db526c7..ae442f970d 100644 --- a/plugins/puppetdb/src/api/index.ts +++ b/plugins/puppetdb/src/api/index.ts @@ -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'; diff --git a/plugins/puppetdb/src/api/types.ts b/plugins/puppetdb/src/api/types.ts index e709b403ac..d0ce697481 100644 --- a/plugins/puppetdb/src/api/types.ts +++ b/plugins/puppetdb/src/api/types.ts @@ -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({ 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; + /** + * 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; + /** + * Get a specific PuppetDB report. + * + * @param puppetDbReportHash - The ID of the report. + * @returns A specific PuppetDB report. + */ + getPuppetDbReport( + puppetDbReportHash: string, + ): Promise; }; diff --git a/plugins/puppetdb/src/components/PuppetDbNodeCard/PuppetDbNodeCard.tsx b/plugins/puppetdb/src/components/Node/NodeCard/NodeCard.tsx similarity index 79% rename from plugins/puppetdb/src/components/PuppetDbNodeCard/PuppetDbNodeCard.tsx rename to plugins/puppetdb/src/components/Node/NodeCard/NodeCard.tsx index 64d068c5c7..7c3a63bf4b 100644 --- a/plugins/puppetdb/src/components/PuppetDbNodeCard/PuppetDbNodeCard.tsx +++ b/plugins/puppetdb/src/components/Node/NodeCard/NodeCard.tsx @@ -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 ( - - ${JSON.stringify(value)} + +
{JSON.stringify(value, null, 2)}
); }; diff --git a/plugins/puppetdb/src/components/PuppetDbNodeCard/index.ts b/plugins/puppetdb/src/components/Node/index.ts similarity index 91% rename from plugins/puppetdb/src/components/PuppetDbNodeCard/index.ts rename to plugins/puppetdb/src/components/Node/index.ts index b5f8184ce1..11d8de14fe 100644 --- a/plugins/puppetdb/src/components/PuppetDbNodeCard/index.ts +++ b/plugins/puppetdb/src/components/Node/index.ts @@ -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'; diff --git a/plugins/puppetdb/src/components/PuppetDbTab/PuppetDbTab.tsx b/plugins/puppetdb/src/components/PuppetDbTab/PuppetDbTab.tsx index e9b55ad7c5..78a830db85 100644 --- a/plugins/puppetdb/src/components/PuppetDbTab/PuppetDbTab.tsx +++ b/plugins/puppetdb/src/components/PuppetDbTab/PuppetDbTab.tsx @@ -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 ( - + ); diff --git a/plugins/puppetdb/src/components/Reports/ReportsTable/ReportsTable.tsx b/plugins/puppetdb/src/components/Reports/ReportsTable/ReportsTable.tsx new file mode 100644 index 0000000000..b927d3ff59 --- /dev/null +++ b/plugins/puppetdb/src/components/Reports/ReportsTable/ReportsTable.tsx @@ -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 ; + } + + const columns: TableColumn[] = [ + { + title: 'Configuration Version', + field: 'configuration_version', + render: rowData => ( + + {rowData.configuration_version} + + ), + }, + { + title: 'Start Time', + field: 'start_time', + align: 'center', + render: rowData => ( + + {new Date(Date.parse(rowData.start_time)).toLocaleString()} + + ), + }, + { + title: 'End Time', + field: 'end_time', + align: 'center', + render: rowData => ( + + {new Date(Date.parse(rowData.end_time)).toLocaleString()} + + ), + }, + { + 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 ( + + {duration.getUTCHours().toString().padStart(2, '0')}: + {duration.getUTCMinutes().toString().padStart(2, '0')}: + {duration.getUTCSeconds().toString().padStart(2, '0')}. + {duration.getUTCMilliseconds().toString().padStart(4, '0')} + + ); + }, + }, + { + title: 'Environment', + field: 'environment', + }, + { + title: 'Mode', + field: 'noop', + align: 'center', + render: rowData => + rowData.noop ? ( + NOOP + ) : ( + NO-NOOP + ), + }, + { + title: 'Status', + field: 'status', + align: 'center', + render: rowData => { + if (rowData.noop) { + return ( + <> + + {rowData.status.charAt(0).toLocaleUpperCase() + + rowData.status.slice(1)} + + ); + } + + switch (rowData.status) { + case 'failed': + return ( + <> + + {rowData.status.charAt(0).toLocaleUpperCase() + + rowData.status.slice(1)} + + ); + case 'changed': + return ( + <> + + {rowData.status.charAt(0).toLocaleUpperCase() + + rowData.status.slice(1)} + + ); + case 'unchanged': + return ( + <> + + {rowData.status.charAt(0).toLocaleUpperCase() + + rowData.status.slice(1)} + + ); + default: + return ( + <> + + {rowData.status.charAt(0).toLocaleUpperCase() + + rowData.status.slice(1)} + + ); + } + }, + }, + ]; + + return ( + + No reports + + } + title={`Latest PuppetDB reports from node ${certName}`} + columns={columns} + data={value || []} + isLoading={loading} + /> + ); + + /* + return ( + + + + +
+ + + Start Time + End Time + Duration + Environment + Configuration Version + Puppet Version + NoOp + Status + + + + {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 ( + {start_date.toLocaleString()} + {end_date.toLocaleString()} + + {duration.getUTCHours().toString().padStart(2, '0')}: + {duration.getUTCMinutes().toString().padStart(2, '0')}: + {duration.getUTCSeconds().toString().padStart(2, '0')}. + {duration.getUTCMilliseconds().toString().padStart(4, '0')} + + {report.environment} + {report.configuration_version} + {report.puppet_version} + {report.noop ? 'Yes' : 'No'} + {report.status} + + )} + )} + +
+ + );*/ +}; diff --git a/plugins/puppetdb/src/routes.ts b/plugins/puppetdb/src/routes.ts index 71ec03134a..171c65f728 100644 --- a/plugins/puppetdb/src/routes.ts +++ b/plugins/puppetdb/src/routes.ts @@ -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, +});