Skip to content

Commit

Permalink
feat(tekton): add support for downloading task and pipelinerun logs (j…
Browse files Browse the repository at this point in the history
…anus-idp#1014)

* feat(tekton): add support for downloading task and pipelinerun logs

* add unit tests

* refactor code and add more unit tests

* add logs streaming in dev mode
  • Loading branch information
karthikjeeyar authored Dec 14, 2023
1 parent 2e4a6ad commit f588292
Show file tree
Hide file tree
Showing 20 changed files with 723 additions and 15 deletions.
2 changes: 2 additions & 0 deletions plugins/shared-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@kubernetes/client-node": "^0.19.0",
"classnames": "^2.3.2",
"date-fns": "^2.30.0",
"file-saver": "^2.0.5",
"lodash": "^4.17.21",
"mathjs": "^11.11.2"
},
Expand All @@ -41,6 +42,7 @@
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.5.1",
"@types/node": "18.18.5",
"@types/file-saver": "2.0.6",
"cross-fetch": "4.0.0",
"msw": "1.3.2"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { saveAs } from 'file-saver';

import { downloadLogFile } from './pod-log-utils';
import { downloadLogFile } from './logs-downloader';

jest.mock('file-saver', () => ({
saveAs: jest.fn(),
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions plugins/shared-react/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './date';
export * from './pipeline';
export * from './unit-conversion';
export * from './downloader/logs-downloader';
19 changes: 19 additions & 0 deletions plugins/tekton/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
EntityKubernetesContent,
KubernetesApi,
kubernetesApiRef,
KubernetesProxyApi,
kubernetesProxyApiRef,
} from '@backstage/plugin-kubernetes';
import { TestApiProvider } from '@backstage/test-utils';

Expand All @@ -31,6 +33,21 @@ const mockEntity: Entity = {
},
};

class MockKubernetesProxyApi implements KubernetesProxyApi {
async getPodLogs(_request: any): Promise<any> {
return new Promise(resolve => {
setTimeout(() => {
resolve({
text: `\nstreaming logs from container: ${_request.containerName} \n...`,
});
}, 500);
});
}

async getEventsByInvolvedObjectName(): Promise<any> {
return {};
}
}
class MockKubernetesClient implements KubernetesApi {
readonly resources;

Expand All @@ -55,6 +72,7 @@ class MockKubernetesClient implements KubernetesApi {
},
);
}

async getWorkloadsByEntity(_request: any): Promise<any> {
return {
items: [
Expand Down Expand Up @@ -129,6 +147,7 @@ createDevApp()
kubernetesApiRef,
new MockKubernetesClient(mockKubernetesPlrResponse),
],
[kubernetesProxyApiRef, new MockKubernetesProxyApi()],
]}
>
<EntityProvider entity={mockEntity}>
Expand Down
39 changes: 39 additions & 0 deletions plugins/tekton/src/__fixtures__/1-pipelinesData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
export const mockKubernetesPlrResponse = {
pods: [
{
metadata: {
name: 'pipeline-test-wbvtlk-tkn-pod',
namespace: 'karthik',
uid: 'bd868fde-1b37-4168-a780-f1772c5924e3',
resourceVersion: '379524',
labels: {
'app.kubernetes.io/managed-by': 'tekton-pipelines',
'backstage.io/kubernetes-id': 'developer-portal',
'janus-idp.io/tekton': 'developer-portal',
'tekton.dev/clusterTask': 'tkn',
'tekton.dev/memberOf': 'tasks',
'tekton.dev/pipeline': 'test-pipeline',
'tekton.dev/pipelineRun': 'pipeline-test-wbvtlk',
'tekton.dev/pipelineTask': 'tkn',
'tekton.dev/taskRun': 'test-pipeline-8e09zm-task1',
},
},
spec: {
volumes: [
{
name: 'tekton-internal-workspace',
emptyDir: {},
},
],
containers: [
{
name: 'step-tkn',
},
],
},
status: {
phase: 'Succeeded',
conditions: [],
startTime: new Date('2023-12-08T12:19:29Z'),
},
},
{
metadata: {
name: 'ruby-ex-git-xf45fo-build-pod',
Expand All @@ -13,6 +50,8 @@ export const mockKubernetesPlrResponse = {
'backstage.io/kubernetes-id': 'backstage',
deployment: 'ruby-ex-git',
'pod-template-hash': '66d547b559',
'tekton.dev/pipelineRun': 'ruby-ex-git-xf45fo',
'tekton.dev/pipelineTask': 'build',
},
ownerReferences: [
{
Expand Down
116 changes: 116 additions & 0 deletions plugins/tekton/src/__fixtures__/pods-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { V1Pod } from '@kubernetes/client-node';

import { PipelineRunKind } from '@janus-idp/shared-react';

export const testPipelineRun: PipelineRunKind = {
apiVersion: 'tekton.dev/v1',
kind: 'PipelineRun',
metadata: {
name: 'test-pipeline-8e09zm',
uid: '17080e46-1ff6-4f15-99e9-e32f603d7cc8',
creationTimestamp: new Date('2023-12-12T06:38:29Z'),
labels: {
'backstage.io/kubernetes-id': 'developer-portal',
'janus-idp.io/tekton': 'developer-portal',
'tekton.dev/build-namespace': 'karthik',
'tekton.dev/pipeline': 'new-pipeline',
},
},
spec: {
pipelineRef: {
name: 'new-pipeline',
},
},
status: {
completionTime: '2023-12-12T06:39:12Z',
pipelineSpec: { tasks: [] },
conditions: [
{
lastTransitionTime: '2023-12-12T06:39:12Z',
message: 'Tasks Completed: 3 (Failed: 0, Cancelled 0), Skipped: 0',
reason: 'Succeeded',
status: 'True',
type: 'Succeeded',
},
],
startTime: '2023-12-12T06:38:29Z',
},
};

export const testPods: V1Pod[] = [
{
metadata: {
name: 'test-pipeline-8e09zm-task1-pod',
namespace: 'karthik',
uid: 'bd868fde-1b37-4168-a780-f1772c5924e3',
resourceVersion: '379524',
labels: {
'app.kubernetes.io/managed-by': 'tekton-pipelines',
'backstage.io/kubernetes-id': 'developer-portal',
'janus-idp.io/tekton': 'developer-portal',
'tekton.dev/clusterTask': 'tkn',
'tekton.dev/memberOf': 'tasks',
'tekton.dev/pipeline': 'test-pipeline',
'tekton.dev/pipelineRun': 'test-pipeline-8e09zm',
'tekton.dev/pipelineTask': 'task1',
'tekton.dev/taskRun': 'test-pipeline-8e09zm-task1',
},
},
spec: {
volumes: [
{
name: 'tekton-internal-workspace',
emptyDir: {},
},
],
containers: [
{
name: 'step-tkn',
},
],
},
status: {
phase: 'Succeeded',
conditions: [],
startTime: new Date('2023-12-08T12:19:29Z'),
},
},
{
metadata: {
name: 'test-pipeline-8e09zm-sbom-task-pod',
namespace: 'karthik',
uid: '055cc13a-bd3e-414e-9eb6-e6cb72870578',
resourceVersion: '379623',
labels: {
'backstage.io/kubernetes-id': 'developer-portal',
'janus-idp.io/tekton': 'developer-portal',
'tekton.dev/pipeline': 'test-pipeline',
'tekton.dev/pipelineRun': 'test-pipeline-8e09zm',
'tekton.dev/pipelineTask': 'sbom-task',
'tekton.dev/task': 'sbom-task',
'tekton.dev/taskRun': 'test-pipeline-8e09zm-sbom-task',
},
},
spec: {
containers: [
{
name: 'step-print-sbom-results',
},
],
},
status: {
phase: 'Succeeded',
conditions: [],

startTime: new Date('2023-12-08T12:19:38Z'),
},
},
];

export const testPipelineRunPods: {
pipelineRun: PipelineRunKind;
pods: V1Pod[];
} = {
pipelineRun: testPipelineRun,
pods: testPods,
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { PipelineRunKind, TaskRunKind } from '@janus-idp/shared-react';

import { tektonGroupColor } from '../../types/types';
import ResourceBadge from '../PipelineRunList/ResourceBadge';
import PipelineRunLogDownloader from './PipelineRunLogDownloader';
import PipelineRunLogs from './PipelineRunLogs';

const useStyles = makeStyles((theme: Theme) =>
Expand Down Expand Up @@ -55,14 +56,22 @@ const PipelineRunLogDialog = ({
}: PipelineRunLogDialogProps) => {
const classes = useStyles();

const [task, setTask] = React.useState(activeTask);

return (
<Dialog maxWidth="xl" fullWidth open={open} onClose={closeDialog}>
<Dialog
data-testid="pipelinerun-logs-dialog"
maxWidth="xl"
fullWidth
open={open}
onClose={closeDialog}
>
<DialogTitle id="pipelinerun-logs" title="PipelineRun Logs">
<Box className={classes.titleContainer}>
<ResourceBadge
color={tektonGroupColor}
abbr="PLR"
name={pipelineRun?.metadata?.name || ''}
name={pipelineRun?.metadata?.name ?? ''}
/>
<IconButton
aria-label="close"
Expand All @@ -75,11 +84,17 @@ const PipelineRunLogDialog = ({
</DialogTitle>
<DialogContent>
<ErrorBoundary>
<PipelineRunLogDownloader
pods={pods}
activeTask={task}
pipelineRun={pipelineRun}
/>
<PipelineRunLogs
pipelineRun={pipelineRun}
taskRuns={taskRuns}
pods={pods}
activeTask={activeTask}
activeTask={task}
setActiveTask={setTask}
/>
</ErrorBoundary>
</DialogContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';

import { V1Pod } from '@kubernetes/client-node';
import { Flex, FlexItem } from '@patternfly/react-core';

import { PipelineRunKind } from '@janus-idp/shared-react';

import {
TEKTON_PIPELINE_RUN,
TEKTON_PIPELINE_TASK,
TEKTON_PIPELINE_TASKRUN,
} from '../../consts/tekton-const';
import PodLogsDownloadLink from './PodLogsDownloadLink';

const PipelineRunLogDownloader: React.FC<{
pods: V1Pod[];
pipelineRun: PipelineRunKind;
activeTask: string | undefined;
}> = ({ pods, pipelineRun, activeTask }) => {
const filteredPods: V1Pod[] = pods?.filter(
(p: V1Pod) =>
p?.metadata?.labels?.[TEKTON_PIPELINE_RUN] ===
pipelineRun?.metadata?.name,
);

const sortedPods: V1Pod[] = React.useMemo(
() =>
Array.from(filteredPods)?.sort(
(a: V1Pod, b: V1Pod) =>
new Date(a?.status?.startTime as Date).getTime() -
new Date(b?.status?.startTime as Date).getTime(),
),
[filteredPods],
);

const activeTaskPod: V1Pod =
sortedPods.find(
(sp: V1Pod) =>
sp.metadata?.labels?.[TEKTON_PIPELINE_TASKRUN] === activeTask,
) ?? sortedPods[sortedPods.length - 1];

return sortedPods.length > 0 ? (
<Flex
data-testid="pipelinerun-logs-downloader"
justifyContent={{ default: 'justifyContentFlexEnd' }}
>
<FlexItem>
<PodLogsDownloadLink
data-testid="download-task-logs"
pods={activeTaskPod ? [activeTaskPod] : []}
fileName={`${
activeTaskPod?.metadata?.labels?.[TEKTON_PIPELINE_TASK] ?? 'task'
}.log`}
downloadTitle="Download"
/>
</FlexItem>
<FlexItem>
<PodLogsDownloadLink
data-testid="download-pipelinerun-logs"
pods={sortedPods}
fileName={`${pipelineRun?.metadata?.name ?? 'pipelinerun'}.log`}
downloadTitle="Download all tasks logs"
/>
</FlexItem>
</Flex>
) : null;
};
export default PipelineRunLogDownloader;
Loading

0 comments on commit f588292

Please sign in to comment.