Skip to content

Commit 8297d54

Browse files
authored
Merge pull request #43 from sahil143/pipeline-run-watch
fix(pipeline-run): open websocket in case of abnormally closed, implement polling, releases watch
2 parents c496807 + 16b17e7 commit 8297d54

17 files changed

+461
-107
lines changed

src/components/Components/ComponentsListView/ComponentListView.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const ComponentListView: React.FC<React.PropsWithChildren<ComponentListViewProps
6262
namespace,
6363
workspace,
6464
applicationName,
65+
true,
6566
);
6667
const [canCreateComponent] = useAccessReviewForModel(ComponentModel, 'create');
6768

src/components/EnterpriseContract/useEnterpriseContractResultFromLogs.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { useTaskRuns } from '../../hooks/useTaskRuns';
33
import { commonFetchJSON, getK8sResourceURL } from '../../k8s';
44
import { PodModel } from '../../models/pod';
5+
import { getPipelineRunFromTaskRunOwnerRef } from '../../utils/common-utils';
56
import { getTaskRunLog } from '../../utils/tekton-results';
67
import { useWorkspaceInfo } from '../Workspace/useWorkspaceInfo';
78
import {
@@ -65,10 +66,12 @@ export const useEnterpriseContractResultFromLogs = (
6566
if (fetchTknLogs) {
6667
const fetch = async () => {
6768
try {
69+
const pid = getPipelineRunFromTaskRunOwnerRef(taskRun[0])?.uid;
6870
const logs = await getTaskRunLog(
6971
workspace,
7072
taskRun[0].metadata.namespace,
71-
taskRun[0].metadata.name,
73+
taskRun[0].metadata.uid,
74+
pid,
7275
);
7376
if (unmount) return;
7477
const json = extractEcResultsFromTaskRunLogs(logs);

src/components/PipelineRun/PipelineRunDetailsView/tabs/PipelineRunDetailsTab.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import {
1616
Bullseye,
1717
Spinner,
1818
} from '@patternfly/react-core';
19-
// import { ErrorDetailsWithStaticLog } from '../../../shared/components/pipeline-run-logs/logs/log-snippet-types';
20-
// import { getPLRLogSnippet } from '../../../shared/components/pipeline-run-logs/logs/pipelineRunLogSnippet';
2119
import { PipelineRunLabel } from '../../../../consts/pipelinerun';
2220
import { usePipelineRun } from '../../../../hooks/usePipelineRuns';
2321
import { useTaskRuns } from '../../../../hooks/useTaskRuns';
@@ -27,6 +25,8 @@ import { RouterParams } from '../../../../routes/utils';
2725
import { Timestamp } from '../../../../shared';
2826
import ErrorEmptyState from '../../../../shared/components/empty-state/ErrorEmptyState';
2927
import ExternalLink from '../../../../shared/components/links/ExternalLink';
28+
import { ErrorDetailsWithStaticLog } from '../../../../shared/components/pipeline-run-logs/logs/log-snippet-types';
29+
import { getPLRLogSnippet } from '../../../../shared/components/pipeline-run-logs/logs/pipelineRunLogSnippet';
3030
import { getCommitSha, getCommitShortName } from '../../../../utils/commits-utils';
3131
import {
3232
calculateDuration,
@@ -82,8 +82,8 @@ const PipelineRunDetailsTab: React.FC = () => {
8282
);
8383
}
8484
const results = getPipelineRunStatusResults(pipelineRun);
85-
const pipelineRunFailed = {} as { title: string; staticMessage: string }; // (getPLRLogSnippet(pipelineRun, taskRuns) ||
86-
//{}) as ErrorDetailsWithStaticLog;
85+
const pipelineRunFailed = (getPLRLogSnippet(pipelineRun, taskRuns) ||
86+
{}) as ErrorDetailsWithStaticLog;
8787
const duration = calculateDuration(
8888
typeof pipelineRun.status?.startTime === 'string' ? pipelineRun.status?.startTime : '',
8989
typeof pipelineRun.status?.completionTime === 'string'

src/hooks/__tests__/useTektonResults.spec.tsx

+27-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// import { QueryClientProvider } from '@tanstack/react-query';
33
// import { act, renderHook as rtlRenderHook } from '@testing-library/react-hooks';
44
import { renderHook } from '@testing-library/react-hooks';
5+
import { PipelineRunModel } from '../../models';
6+
import { TaskRunKind } from '../../types';
57
import {
68
// TektonResultsOptions,
79
getPipelineRuns,
@@ -180,6 +182,16 @@ describe('useTektonResults', () => {
180182
});
181183
});
182184

185+
const mockTR = {
186+
metadata: {
187+
name: 'sample-task-run',
188+
uid: 'sample-task-run-id',
189+
ownerReferences: [
190+
{ kind: PipelineRunModel.kind, uid: 'sample-pipeline-run-id', name: 'sample-pipeline-run' },
191+
],
192+
},
193+
} as TaskRunKind;
194+
183195
describe('useTRTaskRunLog', () => {
184196
it('should not attempt to get task run log', () => {
185197
renderHook(() => useTRTaskRunLog(null, null));
@@ -188,14 +200,19 @@ describe('useTektonResults', () => {
188200
renderHook(() => useTRTaskRunLog('test-ns', null));
189201
expect(getTaskRunLogMock).not.toHaveBeenCalled();
190202

191-
renderHook(() => useTRTaskRunLog(null, 'sample-task-run'));
203+
renderHook(() => useTRTaskRunLog(null, mockTR));
192204
expect(getTaskRunLogMock).not.toHaveBeenCalled();
193205
});
194206

195207
it('should return task run log', async () => {
196208
getTaskRunLogMock.mockReturnValue('sample log');
197-
const { result, waitFor } = renderHook(() => useTRTaskRunLog('test-ns', 'sample-task-run'));
198-
expect(getTaskRunLogMock).toHaveBeenCalledWith('test-ws', 'test-ns', 'sample-task-run');
209+
const { result, waitFor } = renderHook(() => useTRTaskRunLog('test-ns', mockTR));
210+
expect(getTaskRunLogMock).toHaveBeenCalledWith(
211+
'test-ws',
212+
'test-ns',
213+
'sample-task-run-id',
214+
'sample-pipeline-run-id',
215+
);
199216
expect(result.current).toEqual([null, false, undefined]);
200217
await waitFor(() => result.current[1]);
201218
expect(result.current).toEqual(['sample log', true, undefined]);
@@ -206,8 +223,13 @@ describe('useTektonResults', () => {
206223
getTaskRunLogMock.mockImplementation(() => {
207224
throw error;
208225
});
209-
const { result } = renderHook(() => useTRTaskRunLog('test-ns', 'sample-task-run'));
210-
expect(getTaskRunLogMock).toHaveBeenCalledWith('test-ws', 'test-ns', 'sample-task-run');
226+
const { result } = renderHook(() => useTRTaskRunLog('test-ns', mockTR));
227+
expect(getTaskRunLogMock).toHaveBeenCalledWith(
228+
'test-ws',
229+
'test-ns',
230+
'sample-task-run-id',
231+
'sample-pipeline-run-id',
232+
);
211233
expect(result.current).toEqual([null, false, error]);
212234
});
213235
});

src/hooks/useApplicationReleases.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const useApplicationReleases = (
1919
namespace,
2020
workspace,
2121
isList: true,
22+
watch: true,
2223
},
2324
ReleaseModel,
2425
);

src/hooks/useComponents.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const useComponents = (
3838
namespace: string,
3939
workspace: string,
4040
applicationName: string,
41+
watch?: boolean,
4142
): [ComponentKind[], boolean, unknown] => {
4243
const {
4344
data: components,
@@ -49,6 +50,7 @@ export const useComponents = (
4950
workspace,
5051
namespace,
5152
isList: true,
53+
watch,
5254
},
5355
ComponentModel,
5456
);

src/hooks/useReleases.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const useReleases = (
1212
namespace,
1313
workspace,
1414
isList: true,
15+
watch: true,
1516
},
1617
ReleaseModel,
1718
);
@@ -29,6 +30,7 @@ export const useRelease = (
2930
namespace,
3031
workspace,
3132
name,
33+
watch: true,
3234
},
3335
ReleaseModel,
3436
);

src/hooks/useTektonResults.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { useInfiniteQuery } from '@tanstack/react-query';
33
import { useWorkspaceInfo } from '../components/Workspace/useWorkspaceInfo';
44
import { PipelineRunKind, TaskRunKind } from '../types';
5+
import { getPipelineRunFromTaskRunOwnerRef } from '../utils/common-utils';
56
import {
67
TektonResultsOptions,
78
getTaskRunLog,
@@ -61,16 +62,18 @@ export const useTRTaskRuns = (
6162

6263
export const useTRTaskRunLog = (
6364
namespace: string,
64-
taskRunName: string,
65+
taskRun: TaskRunKind,
6566
): [string, boolean, unknown] => {
6667
const { workspace } = useWorkspaceInfo();
6768
const [result, setResult] = React.useState<[string, boolean, unknown]>([null, false, undefined]);
69+
const taskRunUid = taskRun.metadata.uid;
70+
const pipelineRunUid = getPipelineRunFromTaskRunOwnerRef(taskRun)?.uid;
6871
React.useEffect(() => {
6972
let disposed = false;
70-
if (namespace && taskRunName) {
73+
if (namespace && taskRunUid) {
7174
void (async () => {
7275
try {
73-
const log = await getTaskRunLog(workspace, namespace, taskRunName);
76+
const log = await getTaskRunLog(workspace, namespace, taskRunUid, pipelineRunUid);
7477
if (!disposed) {
7578
setResult([log, true, undefined]);
7679
}
@@ -84,6 +87,6 @@ export const useTRTaskRunLog = (
8487
return () => {
8588
disposed = true;
8689
};
87-
}, [workspace, namespace, taskRunName]);
90+
}, [workspace, namespace, taskRunUid, pipelineRunUid]);
8891
return result;
8992
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { K8sModelCommon } from '../../../types/k8s';
3+
import { watchListResource, watchObjectResource } from '../../watch-utils';
4+
import { useK8sQueryWatch } from '../useK8sQueryWatch';
5+
6+
jest.mock('../../watch-utils', () => ({
7+
watchListResource: jest.fn(),
8+
watchObjectResource: jest.fn(),
9+
}));
10+
11+
const WEBSOCKET_RETRY_COUNT = 3;
12+
const WEBSOCKET_RETRY_DELAY = 2000;
13+
14+
describe('useK8sQueryWatch', () => {
15+
beforeEach(() => {
16+
// Clear all mocks before each test
17+
jest.clearAllMocks();
18+
// Clear the WS Map
19+
(global as unknown as { WS: unknown }).WS = new Map();
20+
});
21+
22+
afterEach(() => {
23+
jest.useRealTimers();
24+
});
25+
26+
const mockWebSocket = {
27+
destroy: jest.fn(),
28+
onClose: jest.fn(),
29+
onError: jest.fn(),
30+
};
31+
32+
const mockResourceInit = {
33+
model: { kind: 'Test', apiGroup: 'test.group', apiVersion: 'v1' } as K8sModelCommon,
34+
queryOptions: {},
35+
};
36+
37+
const mockOptions = { wsPrefix: '/test' };
38+
const mockHashedKey = 'test-key';
39+
40+
it('should initialize websocket for list resource', () => {
41+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
42+
43+
renderHook(() => useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions));
44+
45+
expect(watchListResource).toHaveBeenCalledWith(mockResourceInit, mockOptions);
46+
expect(watchObjectResource).not.toHaveBeenCalled();
47+
});
48+
49+
it('should initialize websocket for single resource', () => {
50+
(watchObjectResource as jest.Mock).mockReturnValue(mockWebSocket);
51+
52+
renderHook(() => useK8sQueryWatch(mockResourceInit, false, mockHashedKey, mockOptions));
53+
54+
expect(watchObjectResource).toHaveBeenCalledWith(mockResourceInit, mockOptions);
55+
expect(watchListResource).not.toHaveBeenCalled();
56+
});
57+
58+
it('should not initialize websocket when resourceInit is null', () => {
59+
renderHook(() => useK8sQueryWatch(null, true, mockHashedKey, mockOptions));
60+
61+
expect(watchListResource).not.toHaveBeenCalled();
62+
expect(watchObjectResource).not.toHaveBeenCalled();
63+
});
64+
65+
it('should clean up websocket on unmount', () => {
66+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
67+
68+
const { unmount } = renderHook(() =>
69+
useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions),
70+
);
71+
72+
unmount();
73+
74+
expect(mockWebSocket.destroy).toHaveBeenCalled();
75+
});
76+
77+
it('should handle websocket close with code 1006 and attempt reconnection', () => {
78+
jest.useFakeTimers();
79+
let closeHandler: (event: { code: number }) => void;
80+
81+
mockWebSocket.onClose.mockImplementation((handler) => {
82+
closeHandler = handler;
83+
});
84+
85+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
86+
87+
renderHook(() => useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions));
88+
89+
// Simulate websocket close with code 1006
90+
act(() => {
91+
closeHandler({ code: 1006 });
92+
});
93+
94+
// First retry
95+
act(() => {
96+
jest.advanceTimersByTime(WEBSOCKET_RETRY_DELAY);
97+
});
98+
99+
expect(watchListResource).toHaveBeenCalledTimes(2);
100+
});
101+
102+
it('should set error state after max retry attempts', () => {
103+
jest.useFakeTimers();
104+
let closeHandler: (event: { code: number }) => void;
105+
106+
mockWebSocket.onClose.mockImplementation((handler) => {
107+
closeHandler = handler;
108+
});
109+
110+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
111+
112+
const { result } = renderHook(() =>
113+
useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions),
114+
);
115+
116+
// Simulate multiple websocket closes
117+
for (let i = 0; i <= WEBSOCKET_RETRY_COUNT; i++) {
118+
act(() => {
119+
closeHandler({ code: 1006 });
120+
// Advance time by retry delay with exponential backoff
121+
jest.advanceTimersByTime(WEBSOCKET_RETRY_DELAY * Math.pow(2, i));
122+
});
123+
}
124+
125+
expect(result.current).toEqual({
126+
code: 1006,
127+
message: 'WebSocket connection failed after multiple attempts',
128+
});
129+
});
130+
131+
it('should handle websocket errors', () => {
132+
let errorHandler: (error: { code: number; message: string }) => void;
133+
134+
mockWebSocket.onError.mockImplementation((handler) => {
135+
errorHandler = handler;
136+
});
137+
138+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
139+
140+
const { result } = renderHook(() =>
141+
useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions),
142+
);
143+
144+
const mockError = { code: 1011, message: 'Test error' };
145+
146+
act(() => {
147+
errorHandler(mockError);
148+
});
149+
150+
expect(result.current).toEqual(mockError);
151+
});
152+
153+
it('should clear error state and retry count on new resourceInit', () => {
154+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
155+
156+
const { rerender, result } = renderHook(
157+
({ resourceInit }) => useK8sQueryWatch(resourceInit, true, mockHashedKey, mockOptions),
158+
{ initialProps: { resourceInit: mockResourceInit } },
159+
);
160+
161+
// Set error state
162+
act(() => {
163+
mockWebSocket.onError.mock.calls[0][0]({ code: 1011, message: 'Test error' });
164+
});
165+
166+
expect(result.current).toBeTruthy();
167+
168+
// Rerender with new resourceInit
169+
rerender({
170+
resourceInit: {
171+
...mockResourceInit,
172+
model: { kind: 'TestNew', apiGroup: 'test.group', apiVersion: 'v1' } as K8sModelCommon,
173+
},
174+
});
175+
176+
expect(result.current).toBeNull();
177+
});
178+
});

0 commit comments

Comments
 (0)