From 073d1fef937a4f4203feaa3f4d2ed891aa9432c4 Mon Sep 17 00:00:00 2001 From: Vladyslav Palyvoda Date: Thu, 28 Nov 2024 17:03:52 +0200 Subject: [PATCH] feat: Add reserve logs (#505) --- src/components/LogViewer/index.tsx | 1 + .../ConfigMap/hooks/useEDPConfigMap.ts | 26 +++++ src/k8s/groups/default/ConfigMap/index.ts | 10 +- .../providers/DynamicData/context.ts | 1 + .../providers/DynamicData/provider.tsx | 104 +++++++++++++++++- .../providers/DynamicData/types.ts | 28 +++++ src/pages/pipeline-details/view.tsx | 21 +++- src/services/api/index.ts | 18 ++- 8 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 src/k8s/groups/default/ConfigMap/hooks/useEDPConfigMap.ts diff --git a/src/components/LogViewer/index.tsx b/src/components/LogViewer/index.tsx index b03016fad..deeeee415 100644 --- a/src/components/LogViewer/index.tsx +++ b/src/components/LogViewer/index.tsx @@ -140,6 +140,7 @@ export function LogViewer(props: LogViewerProps) { } xtermRef.current?.clear(); + xtermRef.current?.write(getJointLogs()); return function cleanup() {}; diff --git a/src/k8s/groups/default/ConfigMap/hooks/useEDPConfigMap.ts b/src/k8s/groups/default/ConfigMap/hooks/useEDPConfigMap.ts new file mode 100644 index 000000000..185fe31d1 --- /dev/null +++ b/src/k8s/groups/default/ConfigMap/hooks/useEDPConfigMap.ts @@ -0,0 +1,26 @@ +import { useQuery, UseQueryOptions } from 'react-query'; +import { getDefaultNamespace } from '../../../../../utils/getDefaultNamespace'; +import { REQUEST_KEY_QUERY_CLUSTER_SECRET_LIST } from '../../Secret/requestKeys'; +import { ConfigMapKubeObject } from '..'; +import { ConfigMapKubeObjectInterface } from '../types'; + +interface UseClusterSecretListQueryProps { + props?: { + namespace?: string; + name: string; + }; + options?: UseQueryOptions; +} + +export const useEDPConfigMapQuery = ({ + props, + options, +}: UseClusterSecretListQueryProps) => { + const namespace = props?.namespace || getDefaultNamespace(); + + return useQuery( + REQUEST_KEY_QUERY_CLUSTER_SECRET_LIST, + () => ConfigMapKubeObject.getItemByName(namespace, props.name), + options + ); +}; diff --git a/src/k8s/groups/default/ConfigMap/index.ts b/src/k8s/groups/default/ConfigMap/index.ts index 0c23757bd..c904cb4b4 100644 --- a/src/k8s/groups/default/ConfigMap/index.ts +++ b/src/k8s/groups/default/ConfigMap/index.ts @@ -1,7 +1,7 @@ -import { K8s } from '@kinvolk/headlamp-plugin/lib'; +import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib'; import { streamResults } from '../../../common/streamResults'; import { ConfigMapKubeObjectConfig } from './config'; -import { StreamListProps } from './types'; +import { ConfigMapKubeObjectInterface, StreamListProps } from './types'; const { name: { pluralForm }, @@ -13,4 +13,10 @@ export class ConfigMapKubeObject extends K8s.configMap.default { const url = `/api/${version}/namespaces/${namespace}/${pluralForm}`; return streamResults(url, dataHandler, errorHandler); } + + static getItemByName(namespace: string, name: string): Promise { + const url = `/api/${version}/namespaces/${namespace}/${pluralForm}/${name}`; + + return ApiProxy.request(url); + } } diff --git a/src/pages/pipeline-details/providers/DynamicData/context.ts b/src/pages/pipeline-details/providers/DynamicData/context.ts index aca89e141..860cc29cc 100644 --- a/src/pages/pipeline-details/providers/DynamicData/context.ts +++ b/src/pages/pipeline-details/providers/DynamicData/context.ts @@ -11,4 +11,5 @@ export const DynamicDataContext = React.createContext { const { namespace, name } = useParams(); @@ -19,6 +29,10 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { const [pipelineRunError, setPipelineRunError] = React.useState(null); React.useEffect(() => { + if (pipelineRunError) { + return; + } + const cancelStream = PipelineRunKubeObject.streamItem({ namespace, name, @@ -31,12 +45,16 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { return () => { cancelStream(); }; - }, [namespace, name]); + }, [namespace, name, pipelineRunError]); const [taskRuns, setTaskRuns] = React.useState(null); const [taskRunErrors, setTaskRunErrors] = React.useState(null); React.useEffect(() => { + if (pipelineRunError) { + return; + } + const cancelStream = TaskRunKubeObject.streamListByPipelineRunName({ namespace, parentPipelineRunName: name, @@ -49,7 +67,7 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { return () => { cancelStream(); }; - }, [namespace, name]); + }, [namespace, name, pipelineRunError]); const [tasks, tasksError] = TaskKubeObject.useList(); @@ -57,6 +75,10 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { const [approvalTasksError, setApprovalTasksError] = React.useState(null); React.useEffect(() => { + if (pipelineRunError) { + return; + } + const cancelStream = ApprovalTaskKubeObject.streamListByPipelineRunName({ namespace, pipelineRunName: name, @@ -69,7 +91,7 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { return () => { cancelStream(); }; - }, [namespace, name]); + }, [namespace, name, pipelineRunError]); const { pipelineRunTasks, pipelineRunTasksByNameMap } = usePipelineRunData({ taskRuns, @@ -78,6 +100,74 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { approvalTasks, }); + const { data: EDPConfigMap } = useEDPConfigMapQuery({ + props: { + name: EDP_CONFIG_CONFIG_MAP_NAME, + }, + options: { + enabled: !pipelineRun && !!pipelineRunError, + }, + }); + + const cluster = Utils.getCluster(); + const token = getToken(cluster); + const apiGatewayUrl = EDPConfigMap?.data?.api_gateway_url; + + const apiService = new ApiServiceBase(apiGatewayUrl, token); + + const opensearchApiService = new OpensearchApiService(apiService); + + const { + data: fallbackLogs, + isLoading: isFallbackLogsLoading, + error: fallbackLogsError, + } = useQuery( + ['openSearchLogs', namespace, name], + () => + apiService.createFetcher( + opensearchApiService.getLogsEndpoint(), + JSON.stringify({ + _source: ['log'], + query: { + bool: { + must: [ + { + match_phrase: { + 'kubernetes.namespace_name': namespace, + }, + }, + { + match_phrase: { + 'kubernetes.labels.tekton_dev/pipelineRun': name, + }, + }, + { + range: { + '@timestamp': { + gte: 'now-1d', + lte: 'now', + }, + }, + }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + size: 500, + }), + 'POST' + ), + { + enabled: !!apiService.apiBaseURL, + } + ); + const DataContextValue = React.useMemo( () => ({ pipelineRun: { @@ -102,9 +192,17 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => { !pipelineRunTasks.allTasks.length, error: taskRunErrors || tasksError || pipelineRunError || approvalTasksError, }, + fallbackLogs: { + data: fallbackLogs, + isLoading: isFallbackLogsLoading, + error: fallbackLogsError as ApiError, + }, }), [ approvalTasksError, + fallbackLogs, + fallbackLogsError, + isFallbackLogsLoading, pipelineRun, pipelineRunError, pipelineRunTasks, diff --git a/src/pages/pipeline-details/providers/DynamicData/types.ts b/src/pages/pipeline-details/providers/DynamicData/types.ts index 5dd34b332..28bea752d 100644 --- a/src/pages/pipeline-details/providers/DynamicData/types.ts +++ b/src/pages/pipeline-details/providers/DynamicData/types.ts @@ -5,8 +5,36 @@ import { import { TaskRunKubeObjectInterface } from '../../../../k8s/groups/Tekton/TaskRun/types'; import { DataProviderValue } from '../../../../types/pages'; +export interface OpensearchResponse { + _shards: { + failed: number; + skipped: number; + successful: number; + total: number; + }; + hits: { + hits: { + _id: string; + _index: string; + _score: any; + _source: { + log: string; + }; + sort: number[]; + }[]; + max_score: any; + total: { + relation: string; + value: string; + }; + }; + timed_out: boolean; + took: number; +} + export interface DynamicDataContextProviderValue { pipelineRun: DataProviderValue; taskRuns: DataProviderValue; pipelineRunData: DataProviderValue; + fallbackLogs: DataProviderValue; } diff --git a/src/pages/pipeline-details/view.tsx b/src/pages/pipeline-details/view.tsx index 3669c4998..5163f6f6a 100644 --- a/src/pages/pipeline-details/view.tsx +++ b/src/pages/pipeline-details/view.tsx @@ -1,8 +1,10 @@ import { Router } from '@kinvolk/headlamp-plugin/lib'; +import { Stack, Typography } from '@mui/material'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ErrorContent } from '../../components/ErrorContent'; import { LoadingWrapper } from '../../components/LoadingWrapper'; +import { LogViewer } from '../../components/LogViewer'; import { PageWrapper } from '../../components/PageWrapper'; import { Section } from '../../components/Section'; import { Tabs } from '../../providers/Tabs/components/Tabs'; @@ -26,6 +28,7 @@ export const PageView = () => { const { pipelineRun, pipelineRunData: { isLoading: pipelineRunDataIsLoading }, + fallbackLogs, } = useDynamicDataContext(); const tabs = useTabs(); @@ -36,7 +39,20 @@ export const PageView = () => { const renderPageContent = React.useCallback(() => { if (pipelineRun.error) { - return ; + return ( + + + + + `${el._source.log}\n`)} + topActions={[Reserve Logs]} + /> + + + + ); } return ( @@ -51,6 +67,9 @@ export const PageView = () => { tabs, activeTab, handleChangeTab, + fallbackLogs.isLoading, + fallbackLogs.data?.hits?.hits, + name, ]); return ( diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 8cb0bfca8..c898475c6 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -6,13 +6,15 @@ export class ApiServiceBase { this.apiBaseURL = apiGatewayUrl; this.headers = new Headers({ Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', }); } - createFetcher(url: string) { + createFetcher(url: string, body?: RequestInit['body'], method: RequestInit['method'] = 'GET') { return fetch(url, { - method: 'GET', + method: method, headers: this.headers, + ...(body && { body }), }) .then((response) => { if (response.ok) { @@ -75,3 +77,15 @@ export class DependencyTrackApiService { ).toString(); } } + +export class OpensearchApiService { + apiService: ApiServiceBase; + + constructor(apiService: ApiServiceBase) { + this.apiService = apiService; + } + + getLogsEndpoint() { + return new URL(`/search/logs`, this.apiService.apiBaseURL).toString(); + } +}