fix spark plugin
This commit is contained in:
parent
150e479b1c
commit
0d0199e0e4
23 changed files with 360 additions and 361 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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" ]
|
||||
|
|
|
@ -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",
|
||||
|
|
60
plugins/apache-spark/pi-argo-workflows.yaml
Normal file
60
plugins/apache-spark/pi-argo-workflows.yaml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
29
plugins/apache-spark/rbac-argo-workflows.yaml
Normal file
29
plugins/apache-spark/rbac-argo-workflows.yaml
Normal 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
|
113
plugins/apache-spark/src/api/index.test.ts
Normal file
113
plugins/apache-spark/src/api/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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(),
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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!} />;
|
||||
};
|
||||
|
|
|
@ -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!);
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
CLUSTER_NAME_ANNOTATION,
|
||||
K8S_LABEL_SELECTOR_ANNOTATION,
|
||||
K8S_NAMESPACE_ANNOTATION,
|
||||
} from '../plugin';
|
||||
} from '../consts';
|
||||
|
||||
export type getAnnotationValuesOutput = {
|
||||
ns: string;
|
||||
|
|
6
plugins/apache-spark/src/consts.ts
Normal file
6
plugins/apache-spark/src/consts.ts
Normal 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';
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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('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>
|
Binary file not shown.
Loading…
Reference in a new issue