wip
This commit is contained in:
parent
7c155abf1a
commit
0e5a38664c
21 changed files with 982 additions and 0 deletions
3
plugins/apache-spark/.eslintrc.js
Normal file
3
plugins/apache-spark/.eslintrc.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
|
||||
extends: ['prettier'],
|
||||
});
|
2
plugins/apache-spark/.prettierignore
Normal file
2
plugins/apache-spark/.prettierignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
build
|
||||
coverage
|
13
plugins/apache-spark/README.md
Normal file
13
plugins/apache-spark/README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# apache-spark
|
||||
|
||||
Welcome to the apache-spark plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
|
||||
## Getting started
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/apache-spark](http://localhost:3000/apache-spark).
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
12
plugins/apache-spark/dev/index.tsx
Normal file
12
plugins/apache-spark/dev/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { apacheSparkPlugin, ApacheSparkPage } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(apacheSparkPlugin)
|
||||
.addPage({
|
||||
element: <ApacheSparkPage />,
|
||||
title: 'Root Page',
|
||||
path: '/apache-spark'
|
||||
})
|
||||
.render();
|
55
plugins/apache-spark/package.json
Normal file
55
plugins/apache-spark/package.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "@internal/plugin-apache-spark",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
},
|
||||
"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/core-components": "^0.13.1",
|
||||
"@backstage/core-plugin-api": "^1.5.1",
|
||||
"@backstage/plugin-catalog-react": "^1.7.0",
|
||||
"@backstage/plugin-kubernetes": "^0.9.2",
|
||||
"@backstage/theme": "^0.3.0",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"react-use": "^17.2.4",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.22.7",
|
||||
"@backstage/core-app-api": "^1.8.0",
|
||||
"@backstage/dev-utils": "^1.0.15",
|
||||
"@backstage/test-utils": "^1.3.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/node": "*",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
57
plugins/apache-spark/pi.yaml
Normal file
57
plugins/apache-spark/pi.yaml
Normal file
|
@ -0,0 +1,57 @@
|
|||
#
|
||||
# Copyright 2017 Google LLC
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
apiVersion: "sparkoperator.k8s.io/v1beta2"
|
||||
kind: SparkApplication
|
||||
metadata:
|
||||
# name: spark-pi
|
||||
generateName: spark-pi
|
||||
namespace: default
|
||||
spec:
|
||||
type: Python
|
||||
pythonVersion: "3"
|
||||
mode: cluster
|
||||
image: "public.ecr.aws/m8u6z8z4/manabu-test:test-spark"
|
||||
imagePullPolicy: Always
|
||||
mainClass: org.apache.spark.examples.SparkPi
|
||||
mainApplicationFile: "local:///opt/spark/examples/src/main/python/pi.ps"
|
||||
sparkVersion: "3.1.1"
|
||||
restartPolicy:
|
||||
type: Never
|
||||
volumes:
|
||||
- name: "test-volume"
|
||||
hostPath:
|
||||
path: "/tmp"
|
||||
type: Directory
|
||||
driver:
|
||||
cores: 1
|
||||
coreLimit: "1200m"
|
||||
memory: "512m"
|
||||
labels:
|
||||
version: 3.1.1
|
||||
serviceAccount: spark
|
||||
volumeMounts:
|
||||
- name: "test-volume"
|
||||
mountPath: "/tmp"
|
||||
executor:
|
||||
cores: 1
|
||||
instances: 1
|
||||
memory: "512m"
|
||||
labels:
|
||||
version: 3.1.1
|
||||
volumeMounts:
|
||||
- name: "test-volume"
|
||||
mountPath: "/tmp"
|
||||
|
35
plugins/apache-spark/rbac.yaml
Normal file
35
plugins/apache-spark/rbac.yaml
Normal file
|
@ -0,0 +1,35 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: spark
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
namespace: default
|
||||
name: spark-role
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: spark-role-binding
|
||||
namespace: default
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: spark
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: spark-role
|
||||
apiGroup: rbac.authorization.k8s.io
|
103
plugins/apache-spark/src/api/index.ts
Normal file
103
plugins/apache-spark/src/api/index.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApacheSpark, ApacheSparkList } from './model';
|
||||
import { KubernetesApi } from '@backstage/plugin-kubernetes';
|
||||
|
||||
export const apacheSparkApiRef = createApiRef<ApacheSparkApi>({
|
||||
id: 'plugin.apachespark',
|
||||
});
|
||||
|
||||
const API_VERSION = 'sparkoperator.k8s.io/v1beta2';
|
||||
const SPARK_APP_PLURAL = 'sparkapplications';
|
||||
const K8s_API_TIMEOUT = 'timeoutSeconds';
|
||||
|
||||
export interface ApacheSparkApi {
|
||||
getSparkApps(
|
||||
clusterName: string | undefined,
|
||||
namespace: string | undefined,
|
||||
labels: string | undefined,
|
||||
): Promise<ApacheSparkList>;
|
||||
|
||||
getSparkApp(
|
||||
clusterName: string | undefined,
|
||||
namespace: string | undefined,
|
||||
name: string,
|
||||
): Promise<ApacheSpark>;
|
||||
}
|
||||
|
||||
export class ApacheSparkClient implements ApacheSparkApi {
|
||||
private kubernetesApi: KubernetesApi;
|
||||
constructor(kubernetesApi: KubernetesApi) {
|
||||
this.kubernetesApi = kubernetesApi;
|
||||
}
|
||||
async getSparkApps(
|
||||
clusterName: string | undefined,
|
||||
namespace: string | undefined,
|
||||
labels: string,
|
||||
): Promise<ApacheSparkList> {
|
||||
const ns = namespace !== undefined ? namespace : 'default';
|
||||
const path = `/apis/${API_VERSION}/namespaces/${ns}/${SPARK_APP_PLURAL}`;
|
||||
const query = new URLSearchParams({
|
||||
[K8s_API_TIMEOUT]: '30',
|
||||
// labelSelector: labels,
|
||||
});
|
||||
const resp = await this.kubernetesApi.proxy({
|
||||
clusterName:
|
||||
clusterName !== undefined ? clusterName : await this.getFirstCluster(),
|
||||
path: `${path}?${query.toString()}`,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
return Promise.reject(
|
||||
`failed to fetch resources: ${resp.status}, ${
|
||||
resp.statusText
|
||||
}, ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
const out = JSON.parse(await resp.text());
|
||||
this.removeManagedField(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
async getSparkApp(
|
||||
clusterName: string | undefined,
|
||||
namespace: string | undefined,
|
||||
name: string,
|
||||
): Promise<ApacheSpark> {
|
||||
const ns = namespace !== undefined ? namespace : 'default';
|
||||
const path = `/apis/${API_VERSION}/namespaces/${ns}/${SPARK_APP_PLURAL}/${name}`;
|
||||
const resp = await this.kubernetesApi.proxy({
|
||||
clusterName:
|
||||
clusterName !== undefined ? clusterName : await this.getFirstCluster(),
|
||||
path: `${path}`,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
return Promise.reject(
|
||||
`failed to fetch resources: ${resp.status}, ${
|
||||
resp.statusText
|
||||
}, ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
const out = JSON.parse(await resp.text());
|
||||
this.removeManagedField(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
async getFirstCluster(): Promise<string> {
|
||||
const clusters = await this.kubernetesApi.getClusters();
|
||||
if (clusters.length > 0) {
|
||||
return Promise.resolve(clusters[0].name);
|
||||
}
|
||||
return Promise.reject('no clusters found in configuration');
|
||||
}
|
||||
|
||||
removeManagedField(spark: any) {
|
||||
if (spark.metadata?.hasOwnProperty('managedFields')) {
|
||||
delete spark.metadata.managedFields;
|
||||
}
|
||||
if (spark.items) {
|
||||
for (const i of spark.items) {
|
||||
this.removeManagedField(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
84
plugins/apache-spark/src/api/model.ts
Normal file
84
plugins/apache-spark/src/api/model.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
export type Metadata = {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
creationTimestamp: string;
|
||||
managedFields?: any;
|
||||
};
|
||||
|
||||
export type Spec = {
|
||||
arguments: string[];
|
||||
batchScheduler: string;
|
||||
driver: {
|
||||
coreLimit: string;
|
||||
coreRequest: string;
|
||||
cores: number;
|
||||
gpu: {
|
||||
name: string;
|
||||
quantity: number;
|
||||
};
|
||||
labels: Record<string, string>;
|
||||
memory: string;
|
||||
memoryOverhead: string;
|
||||
podName: string;
|
||||
schedulerName: string;
|
||||
serviceAccount: string;
|
||||
};
|
||||
executor: {
|
||||
coreLimit: string;
|
||||
coreRequest: string;
|
||||
cores: number;
|
||||
gpu: {
|
||||
name: string;
|
||||
quantity: number;
|
||||
};
|
||||
instances: number;
|
||||
labels: Record<string, string>;
|
||||
memory: string;
|
||||
memoryOverhead: string;
|
||||
schedulerName: string;
|
||||
serviceAccount: string;
|
||||
};
|
||||
image: string;
|
||||
mainClass: string;
|
||||
mode: string;
|
||||
pythonVersion: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type Status = {
|
||||
applicationState: {
|
||||
errorMessage: string;
|
||||
state: string;
|
||||
};
|
||||
driverInfo: {
|
||||
podName: string;
|
||||
webUIAddress: string;
|
||||
webUIIngressAddress: string;
|
||||
webUIIngressName: string;
|
||||
webUIPort: string;
|
||||
webUIServiceName: string;
|
||||
};
|
||||
executionAttempts: number;
|
||||
executorState: Record<string, string>;
|
||||
lastSubmissionAttemptTime: string;
|
||||
sparkApplicationId: string;
|
||||
submissionAttempts: number;
|
||||
submissionID: string;
|
||||
terminationTime: string;
|
||||
};
|
||||
|
||||
export type ApacheSpark = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
};
|
||||
|
||||
export type ApacheSparkList = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
items?: ApacheSpark[];
|
||||
};
|
|
@ -0,0 +1,148 @@
|
|||
import {
|
||||
Progress,
|
||||
StatusError,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { apacheSparkApiRef } from '../../api';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getAnnotationValues } from '../utils';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { ApacheSpark, ApacheSparkList } from '../../api/model';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { createStyles, Drawer, makeStyles, Theme } from '@material-ui/core';
|
||||
import { DrawerContent } from '../DetailedDrawer/DetailedDrawer';
|
||||
|
||||
type TableData = {
|
||||
id: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
applicationState?: string;
|
||||
startedAt?: string;
|
||||
finishedAt?: string;
|
||||
raw: ApacheSpark;
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
field: 'name',
|
||||
},
|
||||
{ title: 'Namespace', field: 'namespace', type: 'string' },
|
||||
{
|
||||
title: 'Application State',
|
||||
field: 'applicationState',
|
||||
},
|
||||
{
|
||||
title: 'StartTime',
|
||||
field: 'startedAt',
|
||||
type: 'datetime',
|
||||
defaultSort: 'desc',
|
||||
},
|
||||
{ title: 'EndTime', field: 'finishedAt', type: 'datetime' },
|
||||
];
|
||||
|
||||
const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
width: '50%',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(2.5),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const ApacheSparkOverviewTable = () => {
|
||||
// const { entity } = useEntity();
|
||||
const apiClient = useApi(apacheSparkApiRef);
|
||||
const [columnData, setColumnData] = useState([] as TableData[]);
|
||||
const [isOpen, toggleDrawer] = useState(false);
|
||||
const [drawerData, setDrawerData] = useState({} as ApacheSpark);
|
||||
const classes = useDrawerStyles();
|
||||
// const { ns, clusterName, labelSelector } = getAnnotationValues(entity);
|
||||
|
||||
const { value, loading, error } = useAsync(
|
||||
async (): Promise<ApacheSparkList> => {
|
||||
return await apiClient.getSparkApps(
|
||||
'cnoe-packaging-2',
|
||||
'default',
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const data = value?.items?.map(val => {
|
||||
let state = {};
|
||||
switch (val.status?.applicationState.state) {
|
||||
case 'RUNNING':
|
||||
state = <StatusRunning>Running</StatusRunning>;
|
||||
break;
|
||||
case 'COMPLETED':
|
||||
state = <StatusOK>COMPLETED</StatusOK>;
|
||||
break;
|
||||
case 'FAILED':
|
||||
state = <StatusError>FAILED</StatusError>;
|
||||
break;
|
||||
default:
|
||||
state = (
|
||||
<StatusPending>
|
||||
'${val.status.applicationState.state}'
|
||||
</StatusPending>
|
||||
);
|
||||
break;
|
||||
}
|
||||
return {
|
||||
id: `${val.metadata.namespace}/${val.metadata.name}`,
|
||||
raw: val,
|
||||
name: val.metadata.name,
|
||||
namespace: val.metadata.namespace,
|
||||
applicationState: state,
|
||||
startedAt: val.metadata.creationTimestamp,
|
||||
finishedAt: val.status?.terminationTime,
|
||||
} as TableData;
|
||||
});
|
||||
if (data && data.length > 0) {
|
||||
setColumnData(data);
|
||||
}
|
||||
}, [value]);
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{`${error}`}</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
options={{
|
||||
padding: 'dense',
|
||||
paging: true,
|
||||
search: true,
|
||||
sorting: true,
|
||||
}}
|
||||
onRowClick={(_event, rowData: TableData | undefined) => {
|
||||
setDrawerData(rowData?.raw!);
|
||||
toggleDrawer(true);
|
||||
}}
|
||||
columns={columns}
|
||||
data={columnData}
|
||||
/>
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
}}
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={() => toggleDrawer(false)}
|
||||
>
|
||||
<DrawerContent toggleDrawer={toggleDrawer} apacheSpark={drawerData} />
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
import { ApacheSpark } from '../../api/model';
|
||||
import {
|
||||
Button,
|
||||
createStyles,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { stringify } from 'yaml';
|
||||
import { CopyTextButton, TabbedLayout } from '@backstage/core-components';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
const useDrawerContentStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
backgroundColor: '#EEEEEE',
|
||||
overflow: 'scroll',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
secondaryAction: {
|
||||
marginLeft: theme.spacing(2.5),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const DrawerContent = ({
|
||||
toggleDrawer,
|
||||
apacheSpark,
|
||||
}: {
|
||||
toggleDrawer: (isOpen: boolean) => void;
|
||||
apacheSpark: ApacheSpark;
|
||||
}) => {
|
||||
const classes = useDrawerContentStyles();
|
||||
const yamlString = stringify(apacheSpark);
|
||||
return (
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="/" title="Manifest">
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<Typography variant="h6">{apacheSpark.metadata.name}</Typography>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close"
|
||||
onClick={() => toggleDrawer(false)}
|
||||
color="inherit"
|
||||
>
|
||||
<Close className={classes.icon} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
<CopyTextButton text={yamlString} tooltipText="Copy" />
|
||||
<pre>{yamlString}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => toggleDrawer(false)}
|
||||
>
|
||||
Primary Action
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.secondaryAction}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => toggleDrawer(false)}
|
||||
>
|
||||
Secondary Action
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</TabbedLayout.Route>
|
||||
<TabbedLayout.Route path="/logs" title="logs">
|
||||
<div>logs!</div>
|
||||
</TabbedLayout.Route>
|
||||
</TabbedLayout>
|
||||
);
|
||||
};
|
14
plugins/apache-spark/src/components/Overvew/Overview.tsx
Normal file
14
plugins/apache-spark/src/components/Overvew/Overview.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Content, Header, HeaderLabel, Page } from '@backstage/core-components';
|
||||
import { ApacheSparkOverviewTable } from '../ApacheSparkOverviewTable/ApacheSparkOverviewTable';
|
||||
import React from 'react';
|
||||
|
||||
export const ApacheSparkOverviewPage = () => (
|
||||
<Page themeId="tool">
|
||||
<Header title="Apache Spark">
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ApacheSparkOverviewTable />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
1
plugins/apache-spark/src/components/Overvew/index.ts
Normal file
1
plugins/apache-spark/src/components/Overvew/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Overview';
|
31
plugins/apache-spark/src/components/utils.ts
Normal file
31
plugins/apache-spark/src/components/utils.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
APACHE_SPARK_LABEL_SELECTOR_ANNOTATION,
|
||||
CLUSTER_NAME_ANNOTATION,
|
||||
K8S_LABEL_SELECTOR_ANNOTATION,
|
||||
K8S_NAMESPACE_ANNOTATION,
|
||||
} from '../plugin';
|
||||
|
||||
export type getAnnotationValuesOutput = {
|
||||
ns: string;
|
||||
clusterName?: string;
|
||||
labelSelector?: string;
|
||||
};
|
||||
|
||||
export function getAnnotationValues(entity: Entity): getAnnotationValuesOutput {
|
||||
const ns =
|
||||
entity.metadata.annotations?.[K8S_NAMESPACE_ANNOTATION] !== undefined
|
||||
? entity.metadata.annotations?.[K8S_NAMESPACE_ANNOTATION]
|
||||
: 'default';
|
||||
const clusterName = entity.metadata.annotations?.[CLUSTER_NAME_ANNOTATION];
|
||||
const labelSelector =
|
||||
entity.metadata?.annotations?.[APACHE_SPARK_LABEL_SELECTOR_ANNOTATION] !==
|
||||
undefined
|
||||
? entity.metadata?.annotations?.[APACHE_SPARK_LABEL_SELECTOR_ANNOTATION]
|
||||
: entity.metadata.annotations?.[K8S_LABEL_SELECTOR_ANNOTATION];
|
||||
return {
|
||||
ns: ns,
|
||||
clusterName: clusterName,
|
||||
labelSelector: labelSelector,
|
||||
};
|
||||
}
|
1
plugins/apache-spark/src/index.ts
Normal file
1
plugins/apache-spark/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { apacheSparkPlugin, ApacheSparkPage } from './plugin';
|
7
plugins/apache-spark/src/plugin.test.ts
Normal file
7
plugins/apache-spark/src/plugin.test.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { apacheSparkPlugin } from './plugin';
|
||||
|
||||
describe('apache-spark', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(apacheSparkPlugin).toBeDefined();
|
||||
});
|
||||
});
|
42
plugins/apache-spark/src/plugin.ts
Normal file
42
plugins/apache-spark/src/plugin.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
import { apacheSparkApiRef, ApacheSparkClient } from './api';
|
||||
import { kubernetesApiRef } from '@backstage/plugin-kubernetes';
|
||||
|
||||
export const APACHE_SPARK_LABEL_SELECTOR_ANNOTATION =
|
||||
'apache-spark/label-selector';
|
||||
export const CLUSTER_NAME_ANNOTATION = 'apache-spark/cluster-name';
|
||||
export const K8S_LABEL_SELECTOR_ANNOTATION =
|
||||
'backstage.io/kubernetes-label-selector';
|
||||
export const K8S_NAMESPACE_ANNOTATION = 'backstage.io/kubernetes-namespace';
|
||||
export const apacheSparkPlugin = createPlugin({
|
||||
id: 'apache-spark',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: apacheSparkApiRef,
|
||||
deps: {
|
||||
kubernetesApi: kubernetesApiRef,
|
||||
},
|
||||
factory: ({ kubernetesApi }) => new ApacheSparkClient(kubernetesApi),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const ApacheSparkPage = apacheSparkPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'ApacheSparkPage',
|
||||
component: () =>
|
||||
import('./components/Overvew').then(m => m.ApacheSparkOverviewPage),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
5
plugins/apache-spark/src/routes.ts
Normal file
5
plugins/apache-spark/src/routes.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'apache-spark',
|
||||
});
|
2
plugins/apache-spark/src/setupTests.ts
Normal file
2
plugins/apache-spark/src/setupTests.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
276
plugins/apache-spark/test/t
Normal file
276
plugins/apache-spark/test/t
Normal file
|
@ -0,0 +1,276 @@
|
|||
<!DOCTYPE html><html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/bootstrap.min.css" type="text/css"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/vis-timeline-graph2d.min.css" type="text/css"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/webui.css" type="text/css"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/timeline-view.css" type="text/css"/><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/sorttable.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/jquery-3.5.1.min.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/vis-timeline-graph2d.min.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/bootstrap.bundle.min.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/initialize-tooltips.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/table.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/timeline-view.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/log-view.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/webui.js"></script><script>setUIRoot('')</script>
|
||||
<script>setAppBasePath('')</script>
|
||||
|
||||
|
||||
<link rel="shortcut icon" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/spark-logo-77x50px-hd.png"></link>
|
||||
<title>PythonPi - Spark Jobs</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light mb-4">
|
||||
<div class="navbar-header">
|
||||
<div class="navbar-brand">
|
||||
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/">
|
||||
<img src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/spark-logo-77x50px-hd.png"/>
|
||||
<span class="version">3.2.1</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav mr-auto"><li class="nav-item active">
|
||||
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/">Jobs</a>
|
||||
</li><li class="nav-item">
|
||||
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/stages/">Stages</a>
|
||||
</li><li class="nav-item">
|
||||
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/storage/">Storage</a>
|
||||
</li><li class="nav-item">
|
||||
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/environment/">Environment</a>
|
||||
</li><li class="nav-item">
|
||||
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/executors/">Executors</a>
|
||||
</li><li class="nav-item">
|
||||
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/SQL/">SQL</a>
|
||||
</li></ul>
|
||||
<span class="navbar-text navbar-right d-none d-md-block">
|
||||
<strong title="PythonPi" class="text-nowrap">PythonPi</strong>
|
||||
<span class="text-nowrap">application UI</span>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h3 style="vertical-align: bottom; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis;">
|
||||
Spark Jobs
|
||||
<sup>
|
||||
(<a data-toggle="tooltip" data-placement="top" title="A job is triggered by an action, like count() or saveAsTextFile(). Click on a job to see information about the stages of tasks inside it.">?</a>)
|
||||
</sup>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
|
||||
<strong>User:</strong>
|
||||
root
|
||||
</li>
|
||||
<li>
|
||||
<strong>Total Uptime:</strong>
|
||||
55 min
|
||||
</li>
|
||||
<li>
|
||||
<strong>Scheduling Mode: </strong>
|
||||
FIFO
|
||||
</li>
|
||||
|
||||
<li id="completed-summary">
|
||||
<a href="#completed"><strong>Completed Jobs:</strong></a>
|
||||
1
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div><span class="expand-application-timeline">
|
||||
<span class="expand-application-timeline-arrow arrow-closed"></span>
|
||||
<a data-toggle="tooltip" title="Shows when jobs started and ended and when executors joined or left. Drag to scroll.
|
||||
Click Enable Zooming and use mouse wheel to zoom in/out." data-placement="top">
|
||||
Event Timeline
|
||||
</a>
|
||||
</span><div id="application-timeline" class="collapsed">
|
||||
|
||||
|
||||
<div class="control-panel">
|
||||
<div id="application-timeline-zoom-lock">
|
||||
<input type="checkbox"></input>
|
||||
<span>Enable zooming</span>
|
||||
</div>
|
||||
</div>
|
||||
</div><script type="text/javascript">
|
||||
drawApplicationTimeline(
|
||||
[
|
||||
{
|
||||
'id': 'executors',
|
||||
'content': '<div>Executors</div><div class="legend-area"><svg width="150px" height="55px"> <rect class="executor-added-legend" x="5px" y="5px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="17px">Added</text> <rect class="executor-removed-legend" x="5px" y="30px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="42px">Removed</text> </svg></div>',
|
||||
},
|
||||
{
|
||||
'id': 'jobs',
|
||||
'content': '<div>Jobs</div><div class="legend-area"><svg width="150px" height="85px"> <rect class="succeeded-job-legend" x="5px" y="5px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="17px">Succeeded</text> <rect class="failed-job-legend" x="5px" y="30px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="42px">Failed</text> <rect class="running-job-legend" x="5px" y="55px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="67px">Running</text> </svg></div>',
|
||||
}
|
||||
]
|
||||
,[
|
||||
{
|
||||
'className': 'job application-timeline-object succeeded',
|
||||
'group': 'jobs',
|
||||
'start': new Date(1688165973401),
|
||||
'end': new Date(1688165987811),
|
||||
'content': '<div class="application-timeline-content"' +
|
||||
'data-html="true" data-placement="top" data-toggle="tooltip"' +
|
||||
'data-title="reduce at \/opt\/spark\/examples\/src\/main\/python\/pi.py:43 (Job 0)<br>' +
|
||||
'Status: SUCCEEDED<br>' +
|
||||
'Submitted: 2023/06/30 22:59:33' +
|
||||
'<br>Completed: 2023/06/30 22:59:47">' +
|
||||
'reduce at \/opt\/spark\/examples\/src\/main\/python\/pi.py:43 (Job 0)</div>'
|
||||
}
|
||||
,
|
||||
{
|
||||
'className': 'executor added',
|
||||
'group': 'executors',
|
||||
'start': new Date(1688165940466),
|
||||
'content': '<div class="executor-event-content"' +
|
||||
'data-toggle="tooltip" data-placement="top"' +
|
||||
'data-title="Executor driver<br>' +
|
||||
'Added at 2023/06/30 22:59:00"' +
|
||||
'data-html="true">Executor driver added</div>'
|
||||
}
|
||||
,
|
||||
{
|
||||
'className': 'executor added',
|
||||
'group': 'executors',
|
||||
'start': new Date(1688165986135),
|
||||
'content': '<div class="executor-event-content"' +
|
||||
'data-toggle="tooltip" data-placement="top"' +
|
||||
'data-title="Executor 1<br>' +
|
||||
'Added at 2023/06/30 22:59:46"' +
|
||||
'data-html="true">Executor 1 added</div>'
|
||||
}
|
||||
], 1688165935501, 0);
|
||||
</script><span id="completed" class="collapse-aggregated-completedJobs collapse-table" onclick="collapseTable('collapse-aggregated-completedJobs','aggregated-completedJobs')">
|
||||
<h4>
|
||||
<span class="collapse-table-arrow arrow-open"></span>
|
||||
<a>Completed Jobs (1)</a>
|
||||
</h4>
|
||||
</span><div class="aggregated-completedJobs collapsible-table">
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<form id="form-completedJob-table-top-page" method="get" action="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&completedJob.sort=Job+Id&completedJob.desc=true#completed" class="form-inline float-right justify-content-end" style="margin-bottom: 0px;">
|
||||
<input type="hidden" name="completedJob.sort" value="Job Id"/><input type="hidden" name="completedJob.desc" value="true"/>
|
||||
<label>1 Pages. Jump to</label>
|
||||
<input type="text" name="completedJob.page" id="form-completedJob-table-top-page-no" value="1" class="col-1 form-control"/>
|
||||
|
||||
<label>. Show </label>
|
||||
<input type="text" id="form-completedJob-table-top-page-size" name="completedJob.pageSize" value="100" class="col-1 form-control"/>
|
||||
<label>items in a page.</label>
|
||||
|
||||
<button type="submit" class="btn btn-spark">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<span style="float: left; padding-top: 4px; padding-right: 4px;">Page: </span>
|
||||
<ul class="pagination">
|
||||
|
||||
|
||||
<li class="page-item disabled"><a href="" class="page-link">1</a></li>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-bordered table-sm table-striped table-head-clickable table-cell-width-limited" id="completedJob-table">
|
||||
<thead>
|
||||
<tr><th>
|
||||
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&completedJob.sort=Job+Id&completedJob.desc=false&completedJob.pageSize=100#completed">
|
||||
<span data-toggle="tooltip" data-placement="top" title="">
|
||||
Job Id ▾
|
||||
</span>
|
||||
</a>
|
||||
</th><th>
|
||||
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&completedJob.sort=Description&completedJob.pageSize=100#completed">
|
||||
<span data-toggle="tooltip" data-placement="top" title="">
|
||||
Description
|
||||
</span>
|
||||
</a>
|
||||
</th><th>
|
||||
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&completedJob.sort=Submitted&completedJob.pageSize=100#completed">
|
||||
<span data-toggle="tooltip" data-placement="top" title="">
|
||||
Submitted
|
||||
</span>
|
||||
</a>
|
||||
</th><th>
|
||||
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&completedJob.sort=Duration&completedJob.pageSize=100#completed">
|
||||
<span data-toggle="tooltip" data-placement="top" title="Elapsed time since the job was submitted until execution completion of all its stages.">
|
||||
Duration
|
||||
</span>
|
||||
</a>
|
||||
</th><th>
|
||||
<span data-toggle="tooltip" data-placement="top" title="">
|
||||
Stages: Succeeded/Total
|
||||
</span>
|
||||
</th><th>
|
||||
<span data-toggle="tooltip" data-placement="top" title="">
|
||||
Tasks (for all stages): Succeeded/Total
|
||||
</span>
|
||||
</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="job-0">
|
||||
<td>
|
||||
0
|
||||
</td>
|
||||
<td>
|
||||
<span class="description-input">reduce at /opt/spark/examples/src/main/python/pi.py:43</span>
|
||||
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/job/?id=0" class="name-link">reduce at /opt/spark/examples/src/main/python/pi.py:43</a>
|
||||
</td>
|
||||
<td>
|
||||
2023/06/30 22:59:33
|
||||
</td>
|
||||
<td>14 s</td>
|
||||
<td class="stage-progress-cell">
|
||||
1/1
|
||||
|
||||
|
||||
</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress">
|
||||
<span style="text-align:center; position:absolute; width:100%;">
|
||||
2/2
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
<div class="progress-bar progress-completed" style="width: 100.0%"></div>
|
||||
<div class="progress-bar progress-started" style="width: 0.0%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<div>
|
||||
<form id="form-completedJob-table-bottom-page" method="get" action="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&completedJob.sort=Job+Id&completedJob.desc=true#completed" class="form-inline float-right justify-content-end" style="margin-bottom: 0px;">
|
||||
<input type="hidden" name="completedJob.sort" value="Job Id"/><input type="hidden" name="completedJob.desc" value="true"/>
|
||||
<label>1 Pages. Jump to</label>
|
||||
<input type="text" name="completedJob.page" id="form-completedJob-table-bottom-page-no" value="1" class="col-1 form-control"/>
|
||||
|
||||
<label>. Show </label>
|
||||
<input type="text" id="form-completedJob-table-bottom-page-size" name="completedJob.pageSize" value="100" class="col-1 form-control"/>
|
||||
<label>items in a page.</label>
|
||||
|
||||
<button type="submit" class="btn btn-spark">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<span style="float: left; padding-top: 4px; padding-right: 4px;">Page: </span>
|
||||
<ul class="pagination">
|
||||
|
||||
|
||||
<li class="page-item disabled"><a href="" class="page-link">1</a></li>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
plugins/apache-spark/test/t.tgz
Normal file
BIN
plugins/apache-spark/test/t.tgz
Normal file
Binary file not shown.
Loading…
Reference in a new issue