Skip to content

Commit 9df3fed

Browse files
authored
[velero] Add Velero Plugin (#601)
Add a Velero plugin which can be used to view backups, restores and schedules for Velero. The plugin can also be used to create new backups. For this a template can be specified via the "frontendOptions.backupTemplate" paramter. This template is shown to the user and can be adjusted before the backup is created.
1 parent 422713a commit 9df3fed

30 files changed

+2109
-14
lines changed

.github/workflows/continuous-integration.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
- sonarqube
7777
- sql
7878
- techdocs
79+
- velero
7980
defaults:
8081
run:
8182
working-directory: app

app/package-lock.json

+49-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/packages/app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@kobsio/sonarqube": "*",
4040
"@kobsio/sql": "*",
4141
"@kobsio/techdocs": "*",
42+
"@kobsio/velero": "*",
4243
"@mui/icons-material": "^5.11.0",
4344
"@mui/lab": "^5.0.0-alpha.121",
4445
"@mui/material": "^5.11.7",

app/packages/app/src/main.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import SignalSciences from '@kobsio/signalsciences';
2020
import SonarQube from '@kobsio/sonarqube';
2121
import SQL from '@kobsio/sql';
2222
import TechDocs from '@kobsio/techdocs';
23+
import Velero from '@kobsio/velero';
2324
import { StrictMode } from 'react';
2425
import ReactDOM from 'react-dom/client';
2526

@@ -56,6 +57,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
5657
SonarQube,
5758
SQL,
5859
TechDocs,
60+
Velero,
5961
]}
6062
/>
6163
</StrictMode>,

app/packages/core/src/context/APIContext.tsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { FunctionComponent, createContext, ReactNode } from 'react';
33
import { IReference } from '../crds/dashboard';
44
import { IPermissions, INavigation } from '../crds/user';
55

6+
type ResponseBodyFormat = 'json' | 'text';
7+
68
/**
79
* `RequestOptions` defines the type for the options which can be passed to an API call. These can be an object for
810
* which is send in the `body` of a request, a set of `headers` and much more.
911
*/
10-
type RequestOptions = Omit<RequestInit, 'method' | 'body'> & { body?: unknown };
12+
type RequestOptions = Omit<RequestInit, 'method' | 'body'> & {
13+
body?: unknown;
14+
responseBodyFormat?: ResponseBodyFormat;
15+
};
1116

1217
/**
1318
* `APIError` is the error which is returned by our `APIClient` when a http request fails. It contains a `error` message
@@ -71,11 +76,16 @@ export class APIClient implements IAPIClient {
7176
return undefined;
7277
}
7378

74-
const json = await res.json();
75-
7679
if (res.status >= 200 && res.status < 300) {
80+
if (opts?.responseBodyFormat === 'text') {
81+
const text = await res.text();
82+
return text;
83+
}
84+
85+
const json = await res.json();
7786
return json;
7887
} else {
88+
const json = await res.json();
7989
if (json.errors) {
8090
throw new APIError(json.errors, res.status);
8191
} else {

app/packages/velero/package.json

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "@kobsio/velero",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"files": [
7+
"dist"
8+
],
9+
"module": "./dist/index.js",
10+
"types": "./dist/index.d.ts",
11+
"exports": {
12+
".": {
13+
"import": "./dist/index.js"
14+
}
15+
},
16+
"scripts": {
17+
"analyze": "source-map-explorer 'dist/**/*.js' --no-border-checks",
18+
"build": "tsc && vite build",
19+
"lint": "eslint 'src/**/*.{js,ts,tsx}'",
20+
"test": "vitest run",
21+
"test:coverage": "vitest run --coverage",
22+
"test:watch": "vitest"
23+
},
24+
"peerDependencies": {
25+
"@emotion/react": "^11.10.5",
26+
"@emotion/styled": "^11.10.5",
27+
"@kobsio/core": "*",
28+
"@mui/icons-material": "^5.11.0",
29+
"@mui/lab": "^5.0.0-alpha.121",
30+
"@mui/material": "^5.11.7",
31+
"@tanstack/react-query": "^4.24.4",
32+
"react": "^18.2.0",
33+
"react-dom": "^18.2.0",
34+
"react-router-dom": "^6.8.0",
35+
"victory": "^36.6.8"
36+
},
37+
"devDependencies": {
38+
"@testing-library/jest-dom": "^5.16.5",
39+
"@testing-library/react": "^14.0.0",
40+
"@testing-library/user-event": "^14.4.3",
41+
"@types/node": "^20.4.8",
42+
"@types/react": "^18.0.27",
43+
"@types/react-dom": "^18.0.10",
44+
"@types/react-router-dom": "^5.3.3",
45+
"@vitejs/plugin-react": "^4.0.4",
46+
"@vitest/coverage-v8": "^0.34.1",
47+
"@vitest/ui": "^0.34.1",
48+
"jsdom": "^22.1.0",
49+
"typescript": "^5.1.6",
50+
"vite": "^4.4.9",
51+
"vite-plugin-dts": "^3.5.1",
52+
"vitest": "^0.34.1"
53+
},
54+
"dependencies": {
55+
"@kubernetes/client-node": "^0.18.1",
56+
"jsonpath-plus": "^7.2.0"
57+
}
58+
}
89.5 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
APIContext,
3+
APIError,
4+
DetailsDrawer,
5+
Editor,
6+
IAPIContext,
7+
IPluginInstance,
8+
UseQueryWrapper,
9+
} from '@kobsio/core';
10+
import { Box, Card, CardContent, Tab, Tabs } from '@mui/material';
11+
import { useQuery } from '@tanstack/react-query';
12+
import yaml from 'js-yaml';
13+
import { FunctionComponent, useContext, useState } from 'react';
14+
15+
import { IVeleroResource } from '../utils/utils';
16+
17+
const ResourceDetailsLogs: FunctionComponent<{
18+
cluster: string;
19+
instance: IPluginInstance;
20+
name: string;
21+
namespace: string;
22+
veleroResource: IVeleroResource;
23+
}> = ({ instance, veleroResource, cluster, namespace, name }) => {
24+
const apiContext = useContext<IAPIContext>(APIContext);
25+
26+
const { isError, isLoading, error, data, refetch } = useQuery<string, APIError>(
27+
['velero/logs', instance, veleroResource, cluster, namespace, name],
28+
async () => {
29+
const result = await apiContext.client.get<string>(
30+
`/api/plugins/velero/logs?namespace=${namespace}&name=${name}&type=${veleroResource.type}`,
31+
{
32+
headers: {
33+
'x-kobs-cluster': cluster,
34+
'x-kobs-plugin': instance.name,
35+
},
36+
responseBodyFormat: 'text',
37+
},
38+
);
39+
40+
return result;
41+
},
42+
);
43+
44+
return (
45+
<UseQueryWrapper
46+
error={error}
47+
errorTitle="Failed to get logs"
48+
isError={isError}
49+
isLoading={isLoading}
50+
isNoData={!data}
51+
noDataTitle="No logs were found"
52+
refetch={refetch}
53+
>
54+
{data ? (
55+
<Card>
56+
<CardContent style={{ overflowX: 'scroll', whiteSpace: 'pre', width: '100%' }}>{data}</CardContent>
57+
</Card>
58+
) : null}
59+
</UseQueryWrapper>
60+
);
61+
};
62+
63+
/**
64+
* The `ResourceDetails` drawer is used to display a drawer with some details for each Velero resource. Depending on the
65+
* provided `veleroResource` different information will be displayed. It is also possible to access the actions for a
66+
* resource from the drawer.
67+
*/
68+
const ResourceDetails: FunctionComponent<{
69+
cluster: string;
70+
instance: IPluginInstance;
71+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
72+
manifest: any;
73+
name: string;
74+
namespace: string;
75+
onClose: () => void;
76+
open: boolean;
77+
path: string;
78+
refetch: () => void;
79+
resource: string;
80+
veleroResource: IVeleroResource;
81+
}> = ({ instance, veleroResource, cluster, namespace, name, manifest, resource, path, refetch, onClose, open }) => {
82+
const [activeTab, setActiveTab] = useState<string>('yaml');
83+
84+
return (
85+
<DetailsDrawer
86+
size="large"
87+
open={open}
88+
onClose={onClose}
89+
title={name}
90+
subtitle={namespace ? `(${cluster} / ${namespace})` : `(${cluster})`}
91+
>
92+
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
93+
<Tabs variant="scrollable" scrollButtons={false} value={activeTab} onChange={(_, value) => setActiveTab(value)}>
94+
<Tab key="yaml" label="Yaml" value="yaml" />
95+
<Tab key="logs" label="Logs" value="logs" />
96+
</Tabs>
97+
</Box>
98+
99+
<Box key="yaml" hidden={activeTab !== 'yaml'} py={6}>
100+
{activeTab === 'yaml' && <Editor language="yaml" readOnly={true} value={yaml.dump(manifest)} />}
101+
</Box>
102+
103+
<Box key="logs" hidden={activeTab !== 'logs'} py={6}>
104+
{activeTab === 'logs' && (
105+
<ResourceDetailsLogs
106+
instance={instance}
107+
veleroResource={veleroResource}
108+
cluster={cluster}
109+
namespace={namespace}
110+
name={name}
111+
/>
112+
)}
113+
</Box>
114+
</DetailsDrawer>
115+
);
116+
};
117+
118+
export default ResourceDetails;

0 commit comments

Comments
 (0)