fix spark plugin

This commit is contained in:
Manabu Mccloskey 2023-07-21 09:00:55 -07:00
parent 150e479b1c
commit 0d0199e0e4
23 changed files with 360 additions and 361 deletions

View file

@ -11,10 +11,9 @@ This plugin allows you to display information related to your Apache Spark Appli
### Configuration
Entities must be annotated with Kubernetes annotations. For example:
[The Kubernetes plugin](https://backstage.io/docs/features/kubernetes/) must also be installed and enabled.
Entities must be annotated with Kubernetes annotations. For example:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component

View file

@ -121,7 +121,7 @@ spec:
name: Register
action: catalog:register
input:
catalogInfoPath: 'catalog-info.yaml'
catalogInfoPath: '/catalog-info.yaml'
repoContentsUrl: ${{ steps['init-repo'].output.repoContentsUrl }}
output:
links:

View file

@ -1,7 +1,7 @@
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: "spark-${{values.name}}-"
name: "spark-${{values.name}}"
namespace: "${{values.namespace}}"
labels:
backstage.io/component-id: "${{values.name}}"
@ -15,16 +15,20 @@ spec:
templates:
- name: demo-workflow
steps:
- - name: sleep
template: sleep
- - name: spark-operator
template: sparkapp
- name: sleep
- - name: prepare-resources
template: prepare-resources
- - name: run-sparkapp
template: run-sparkapp
- - name: cleanup-resources
template: cleanup-resources
- name: notify-users
template: cleanup-resources
- name: prepare-resources
container:
image: docker/whalesay
command: [ sleep ]
args: [ "60" ]
- name: sparkapp
args: [ "10" ]
- name: run-sparkapp
resource:
action: create
setOwnerReference: true
@ -32,3 +36,8 @@ spec:
failureCondition: status.applicationState.state in (FAILED, ERROR)
manifest: |
${{values.manifest | dump}}
- name: cleanup-resources
container:
image: docker/whalesay
command: [ sleep ]
args: [ "5" ]

View file

@ -17,12 +17,13 @@
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"test": "backstage-cli package test --watch false",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^1.4.1",
"@backstage/core-components": "^0.13.1",
"@backstage/core-plugin-api": "^1.5.1",
"@backstage/plugin-catalog-react": "^1.7.0",
@ -31,7 +32,11 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"react": "^17.0.0",
"react-dom": "^16.13.1 || ^17.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0",
"react-use": "^17.2.4",
"typescript": "^3.7.5 || ^4.0.0 || ^5.0.0",
"yaml": "^2.3.1"
},
"peerDependencies": {
@ -43,6 +48,7 @@
"@backstage/core-app-api": "^1.8.0",
"@backstage/dev-utils": "^1.0.15",
"@backstage/test-utils": "^1.3.1",
"@testing-library/dom": ">=7.21.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",

View file

@ -0,0 +1,60 @@
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
name: spark-operator
namespace: default
spec:
arguments: {}
entrypoint: demo-workflow
serviceAccountName: argo-workflows
templates:
- name: demo-workflow
steps:
- - name: sleep
template: sleep
- - name: spark-operator
template: sparkapp
- name: sleep
container:
image: docker/whalesay
command: [ sleep ]
args: [ "60" ]
- name: sparkapp
resource:
action: create
setOwnerReference: true
successCondition: status.applicationState.state == COMPLETED
failureCondition: status.applicationState.state in (FAILED, ERROR)
manifest: |
apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
generateName: pyspark-pi-
namespace: default
spec:
type: Python
pythonVersion: "3"
mode: cluster
image: "public.ecr.aws/r1l5w1y9/spark-operator:3.2.1-hadoop-3.3.1-java-11-scala-2.12-python-3.8-latest"
mainApplicationFile: "local:///opt/spark/examples/src/main/python/pi.py"
sparkVersion: "3.1.1"
restartPolicy:
type: OnFailure
onFailureRetries: 1
onFailureRetryInterval: 10
onSubmissionFailureRetries: 1
onSubmissionFailureRetryInterval: 20
driver:
cores: 1
coreLimit: "1200m"
memory: "512m"
labels:
version: 3.1.1
serviceAccount: spark
executor:
cores: 1
instances: 2
memory: "512m"
serviceAccount: spark
labels:
version: 3.1.1

View file

@ -1,23 +1,7 @@
#
# 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
generateName: spark-pi-
namespace: default
spec:
type: Python

View file

@ -1,23 +1,9 @@
#
# 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
generateName: spark-pi-
namespace: default
spec:
type: Python

View file

@ -0,0 +1,29 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo-workflows
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: argo-workflows-spark-full-control
rules:
- apiGroups: ["sparkoperator.k8s.io"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argo-workflows-spark
namespace: default
subjects:
- kind: ServiceAccount
name: argo-workflows
namespace: default
roleRef:
kind: Role
name: argo-workflows-spark-full-control
apiGroup: rbac.authorization.k8s.io

View file

@ -0,0 +1,113 @@
import { ApacheSparkClient } from './index';
import { ApacheSpark } from './model';
const mockKubernetesApi = {
proxy: jest.fn(),
getClusters: jest.fn(),
getObjectsByEntity: jest.fn(),
getWorkloadsByEntity: jest.fn(),
getCustomObjectsByEntity: jest.fn(),
};
describe('ApacheSparkClient', () => {
let apacheSparkClient: ApacheSparkClient;
beforeEach(() => {
apacheSparkClient = new ApacheSparkClient(mockKubernetesApi);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should fetch Spark application logs', async () => {
mockKubernetesApi.proxy.mockResolvedValue({
ok: true,
text: () => {
return 'logs';
},
});
const logs = await apacheSparkClient.getLogs(
'cluster1',
'spark-namespace',
'spark-pod-name',
'abc',
);
expect(logs).toEqual('logs');
expect(mockKubernetesApi.proxy).toHaveBeenCalledWith({
clusterName: 'cluster1',
path: '/api/v1/namespaces/spark-namespace/pods/spark-pod-name/log?tailLines=1000&container=abc',
});
});
it('should throw error if Spark application logs are not fetched', async () => {
mockKubernetesApi.proxy.mockResolvedValueOnce({
status: 500,
statusText: 'Internal Server Error',
ok: false,
text: () => {
return 'oh noes';
},
});
await expect(
apacheSparkClient.getLogs(
'spark-app-name',
'spark-namespace',
'spark-pod-name',
'abc',
),
).rejects.toEqual(
'failed to fetch logs: 500, Internal Server Error, oh noes',
);
});
// test getSparkApp method
it('should fetch Spark application', async () => {
// @ts-ignore
const mockResponse: ApacheSpark = {
apiVersion: 'sparkoperator.k8s.io/v1beta2',
kind: 'SparkApplication',
metadata: {
name: 'spark-app-name',
namespace: 'spark-namespace',
labels: {
app: 'spark-app-name',
},
creationTimestamp: '2021-01-01T00:00:00Z',
},
spec: {
image: 'abc',
mainApplicationFile: 'main.py',
mode: 'cluster',
sparkVersion: 'v3.1.1.',
type: 'Python',
driver: {
cores: 1,
},
executor: {
cores: 1,
},
},
status: {
applicationState: {
state: 'RUNNING',
},
},
};
mockKubernetesApi.proxy.mockResolvedValue({
ok: true,
text: () => {
return JSON.stringify(mockResponse);
},
});
const application = await apacheSparkClient.getSparkApp(
'spark-app-name',
'spark-namespace',
'abc',
);
expect(application).toEqual(mockResponse);
});
});

View file

@ -46,14 +46,16 @@ export class ApacheSparkClient implements ApacheSparkApi {
async getSparkApps(
clusterName: string | undefined,
namespace: string | undefined,
labels: string,
labels: string | undefined,
): 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,
});
if (labels) {
query.set('labelSelector', labels);
}
const resp = await this.kubernetesApi.proxy({
clusterName:
clusterName !== undefined ? clusterName : await this.getFirstCluster(),

View file

@ -1,8 +1,8 @@
export type Metadata = {
name: string;
namespace?: string;
labels: Record<string, string>;
annotations: Record<string, string>;
labels?: Record<string, string>;
annotations?: Record<string, string>;
creationTimestamp: string;
managedFields?: any;
};
@ -51,10 +51,10 @@ export type Spec = {
export type Status = {
applicationState: {
errorMessage: string;
errorMessage?: string;
state: string;
};
driverInfo: {
driverInfo?: {
podName: string;
webUIAddress: string;
webUIIngressAddress: string;
@ -62,13 +62,13 @@ export type Status = {
webUIPort: string;
webUIServiceName: string;
};
executionAttempts: number;
executorState: { [key: string]: string };
lastSubmissionAttemptTime: string;
sparkApplicationId: string;
submissionAttempts: number;
submissionID: string;
terminationTime: string;
executionAttempts?: number;
executorState?: { [key: string]: string };
lastSubmissionAttemptTime?: string;
sparkApplicationId?: string;
submissionAttempts?: number;
submissionID?: string;
terminationTime?: string;
};
export type ApacheSpark = {

View file

@ -0,0 +1,83 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { useApi } from '@backstage/core-plugin-api';
import { useEntity } from '@backstage/plugin-catalog-react';
import useAsync from 'react-use/lib/useAsync';
import { ApacheSpark } from '../../api/model';
import { ApacheSparkDriverLogs } from './ApacheSparkLogs';
import {
APACHE_SPARK_LABEL_SELECTOR_ANNOTATION,
CLUSTER_NAME_ANNOTATION,
K8S_NAMESPACE_ANNOTATION,
} from '../../consts';
jest.mock('@backstage/core-plugin-api');
jest.mock('react-use/lib/useAsync');
jest.mock('@backstage/plugin-catalog-react');
jest.mock('@backstage/core-components', () => ({
LogViewer: (props: { text: string }) => {
return <div>{props.text}</div>;
},
}));
describe('ApacheSparkDriverLogs', () => {
const mockUseApi = useApi as jest.MockedFunction<typeof useApi>;
const mockUseAsync = useAsync as jest.MockedFunction<typeof useAsync>;
const mockUseEntity = useEntity as jest.MockedFunction<typeof useEntity>;
const mockGetLogs = jest.fn();
const mockSparkApp = {
status: {
driverInfo: {
podName: 'test-pod',
},
},
} as ApacheSpark;
beforeEach(() => {
mockUseApi.mockReturnValue({
getLogs: mockGetLogs,
});
mockUseEntity.mockReturnValue({
entity: {
apiVersion: 'version',
kind: 'kind',
metadata: {
name: 'name',
namespace: 'ns1',
annotations: {
[K8S_NAMESPACE_ANNOTATION]: 'k8s-ns',
[CLUSTER_NAME_ANNOTATION]: 'my-cluster',
[APACHE_SPARK_LABEL_SELECTOR_ANNOTATION]: 'env=test',
},
},
},
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should render error message if there is an error', () => {
mockUseAsync.mockReturnValue({
value: undefined,
loading: false,
error: new Error('Test error'),
});
render(<ApacheSparkDriverLogs sparkApp={mockSparkApp} />);
expect(screen.getByText('Error: Test error')).toBeInTheDocument();
expect(screen.getByRole('alert')).toBeInTheDocument();
});
it('should render the log viewer with the fetched logs', async () => {
mockUseAsync.mockReturnValue({
value: 'test logs',
loading: false,
error: undefined,
});
render(<ApacheSparkDriverLogs sparkApp={mockSparkApp} />);
expect(screen.getByText('test logs')).toBeInTheDocument();
});
});

View file

@ -11,15 +11,19 @@ import {
} from '@backstage/core-components';
import Alert from '@material-ui/lab/Alert';
import React, { useEffect, useState } from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import { getAnnotationValues } from '../utils';
export const ApacheSparkDriverLogs = (props: { sparkApp: ApacheSpark }) => {
const apiClient = useApi(apacheSparkApiRef);
const { entity } = useEntity();
const { ns, clusterName } = getAnnotationValues(entity);
const { value, loading, error } = useAsync(async (): Promise<string> => {
return await apiClient.getLogs(
'cnoe-packaging-2',
'default',
props.sparkApp.status.driverInfo.podName,
clusterName,
ns,
props.sparkApp.status.driverInfo?.podName!,
'spark-kubernetes-driver',
);
}, [props]);
@ -33,13 +37,16 @@ export const ApacheSparkDriverLogs = (props: { sparkApp: ApacheSpark }) => {
const ExecutorLogs = (props: { name: string }) => {
const apiClient = useApi(apacheSparkApiRef);
const { entity } = useEntity();
const [logs, setLogs] = useState('');
const { ns, clusterName } = getAnnotationValues(entity);
useEffect(() => {
async function getLogs() {
try {
const val = await apiClient.getLogs(
'cnoe-packaging-2',
'default',
clusterName,
ns,
props.name,
'spark-kubernetes-executor',
);
@ -53,7 +60,7 @@ const ExecutorLogs = (props: { name: string }) => {
if (props.name !== '') {
getLogs();
}
}, [apiClient, props]);
}, [apiClient, clusterName, ns, props]);
return <LogViewer text={logs!} />;
};

View file

@ -7,16 +7,16 @@ import {
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';
import { getAnnotationValues } from '../utils';
import { useEntity } from '@backstage/plugin-catalog-react';
type TableData = {
id: string;
@ -28,7 +28,7 @@ type TableData = {
raw: ApacheSpark;
};
const columns: TableColumn[] = [
const columns: TableColumn<TableData>[] = [
{
title: 'Name',
field: 'name',
@ -57,21 +57,17 @@ const useDrawerStyles = makeStyles((theme: Theme) =>
);
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 { entity } = useEntity();
const { ns, clusterName, labelSelector } = getAnnotationValues(entity);
const { value, loading, error } = useAsync(
async (): Promise<ApacheSparkList> => {
return await apiClient.getSparkApps(
'cnoe-packaging-2',
'default',
undefined,
);
return await apiClient.getSparkApps(clusterName, ns, labelSelector);
},
);
@ -124,6 +120,8 @@ export const ApacheSparkOverviewTable = () => {
paging: true,
search: true,
sorting: true,
pageSize: 10,
pageSizeOptions: [5, 10, 20, 50],
}}
onRowClick={(_event, rowData: TableData | undefined) => {
setDrawerData(rowData?.raw!);

View file

@ -1,6 +1,5 @@
import { ApacheSpark } from '../../api/model';
import {
Button,
createStyles,
IconButton,
makeStyles,
@ -87,7 +86,7 @@ export const DrawerContent = ({
</div>
</>
</TabbedLayout.Route>
<TabbedLayout.Route path="/logs" title="Logs">
<TabbedLayout.Route path="/live-logs" title="Live logs">
<>
<div className={classes.logs2}>
<div className={classes.logs}>

View file

@ -1,4 +1,4 @@
import { createStyles, makeStyles, Theme } from '@material-ui/core';
import { createStyles, makeStyles } from '@material-ui/core';
import { ApacheSpark } from '../../api/model';
import {
InfoCard,
@ -47,7 +47,7 @@ function generateMetadata(sparkApp: ApacheSpark): generateMetadataOutput {
}
}
out.app = app;
out.driver = sparkApp.status.driverInfo;
out.driver = sparkApp.status.driverInfo ? sparkApp.status.driverInfo : {};
out.executor = executor;
return out;
}

View file

@ -4,7 +4,7 @@ import {
CLUSTER_NAME_ANNOTATION,
K8S_LABEL_SELECTOR_ANNOTATION,
K8S_NAMESPACE_ANNOTATION,
} from '../plugin';
} from '../consts';
export type getAnnotationValuesOutput = {
ns: string;

View file

@ -0,0 +1,6 @@
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';

View file

@ -8,12 +8,6 @@ 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: {
@ -34,7 +28,7 @@ export const ApacheSparkPage = apacheSparkPlugin.provide(
createRoutableExtension({
name: 'ApacheSparkPage',
component: () =>
import('./components/Overvew').then(m => m.ApacheSparkOverviewPage),
import('./components/Overview').then(m => m.ApacheSparkOverviewPage),
mountPoint: rootRouteRef,
}),
);

View file

@ -1,276 +0,0 @@
<!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(&#39;collapse-aggregated-completedJobs&#39;,&#39;aggregated-completedJobs&#39;)">
<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/?&amp;completedJob.sort=Job+Id&amp;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/?&amp;completedJob.sort=Job+Id&amp;completedJob.desc=false&amp;completedJob.pageSize=100#completed">
<span data-toggle="tooltip" data-placement="top" title="">
Job Id&nbsp;&#x25BE;
</span>
</a>
</th><th>
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Description&amp;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/?&amp;completedJob.sort=Submitted&amp;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/?&amp;completedJob.sort=Duration&amp;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/?&amp;completedJob.sort=Job+Id&amp;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>

Binary file not shown.