Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add reserve logs #506

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/LogViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export function LogViewer(props: LogViewerProps) {
}

xtermRef.current?.clear();

xtermRef.current?.write(getJointLogs());

return function cleanup() {};
Expand Down
26 changes: 26 additions & 0 deletions src/k8s/groups/default/ConfigMap/hooks/useEDPConfigMap.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType> {
props?: {
namespace?: string;
name: string;
};
options?: UseQueryOptions<ConfigMapKubeObjectInterface, Error, ReturnType>;
}

export const useEDPConfigMapQuery = <ReturnType = ConfigMapKubeObjectInterface>({
props,
options,
}: UseClusterSecretListQueryProps<ReturnType>) => {
const namespace = props?.namespace || getDefaultNamespace();

return useQuery<ConfigMapKubeObjectInterface, Error, ReturnType>(
REQUEST_KEY_QUERY_CLUSTER_SECRET_LIST,
() => ConfigMapKubeObject.getItemByName(namespace, props.name),
options
);
};
10 changes: 8 additions & 2 deletions src/k8s/groups/default/ConfigMap/index.ts
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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<ConfigMapKubeObjectInterface> {
const url = `/api/${version}/namespaces/${namespace}/${pluralForm}/${name}`;

return ApiProxy.request(url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const DynamicDataContext = React.createContext<DynamicDataContextProvider
pipelineRun: initialData,
taskRuns: initialData,
pipelineRunData: initialData,
fallbackLogs: initialData,
});
104 changes: 101 additions & 3 deletions src/pages/pipeline-details/providers/DynamicData/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Utils } from '@kinvolk/headlamp-plugin/lib';
import { ApiError } from '@kinvolk/headlamp-plugin/lib/lib/k8s/apiProxy';
import React from 'react';
import { useQuery } from 'react-query';
import { useParams } from 'react-router-dom';
import { EDP_CONFIG_CONFIG_MAP_NAME } from '../../../../k8s/groups/default/ConfigMap/constants';
import { useEDPConfigMapQuery } from '../../../../k8s/groups/default/ConfigMap/hooks/useEDPConfigMap';
import { ApprovalTaskKubeObject } from '../../../../k8s/groups/EDP/ApprovalTask';
import { ApprovalTaskKubeObjectInterface } from '../../../../k8s/groups/EDP/ApprovalTask/types';
import { PipelineRunKubeObject } from '../../../../k8s/groups/Tekton/PipelineRun';
Expand All @@ -9,8 +13,14 @@ import { PipelineRunKubeObjectInterface } from '../../../../k8s/groups/Tekton/Pi
import { TaskKubeObject } from '../../../../k8s/groups/Tekton/Task';
import { TaskRunKubeObject } from '../../../../k8s/groups/Tekton/TaskRun';
import { TaskRunKubeObjectInterface } from '../../../../k8s/groups/Tekton/TaskRun/types';
import { ApiServiceBase, OpensearchApiService } from '../../../../services/api';
import { PipelineRouteParams } from '../../types';
import { DynamicDataContext } from './context';
import { OpensearchResponse } from './types';

function getToken(cluster: string) {
return JSON.parse(localStorage.tokens || '{}')?.[cluster];
}

export const DynamicDataContextProvider: React.FC = ({ children }) => {
const { namespace, name } = useParams<PipelineRouteParams>();
Expand All @@ -19,6 +29,10 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => {
const [pipelineRunError, setPipelineRunError] = React.useState<ApiError | null>(null);

React.useEffect(() => {
if (pipelineRunError) {
return;
}

const cancelStream = PipelineRunKubeObject.streamItem({
namespace,
name,
Expand All @@ -31,12 +45,16 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => {
return () => {
cancelStream();
};
}, [namespace, name]);
}, [namespace, name, pipelineRunError]);

const [taskRuns, setTaskRuns] = React.useState<TaskRunKubeObjectInterface[]>(null);
const [taskRunErrors, setTaskRunErrors] = React.useState<ApiError | null>(null);

React.useEffect(() => {
if (pipelineRunError) {
return;
}

const cancelStream = TaskRunKubeObject.streamListByPipelineRunName({
namespace,
parentPipelineRunName: name,
Expand All @@ -49,14 +67,18 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => {
return () => {
cancelStream();
};
}, [namespace, name]);
}, [namespace, name, pipelineRunError]);

const [tasks, tasksError] = TaskKubeObject.useList();

const [approvalTasks, setApprovalTasks] = React.useState<ApprovalTaskKubeObjectInterface[]>(null);
const [approvalTasksError, setApprovalTasksError] = React.useState<ApiError | null>(null);

React.useEffect(() => {
if (pipelineRunError) {
return;
}

const cancelStream = ApprovalTaskKubeObject.streamListByPipelineRunName({
namespace,
pipelineRunName: name,
Expand All @@ -69,7 +91,7 @@ export const DynamicDataContextProvider: React.FC = ({ children }) => {
return () => {
cancelStream();
};
}, [namespace, name]);
}, [namespace, name, pipelineRunError]);

const { pipelineRunTasks, pipelineRunTasksByNameMap } = usePipelineRunData({
taskRuns,
Expand All @@ -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<OpensearchResponse>(
['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: {
Expand All @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions src/pages/pipeline-details/providers/DynamicData/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PipelineRunKubeObjectInterface>;
taskRuns: DataProviderValue<TaskRunKubeObjectInterface[]>;
pipelineRunData: DataProviderValue<PipelineRunData>;
fallbackLogs: DataProviderValue<OpensearchResponse>;
}
21 changes: 20 additions & 1 deletion src/pages/pipeline-details/view.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,6 +28,7 @@ export const PageView = () => {
const {
pipelineRun,
pipelineRunData: { isLoading: pipelineRunDataIsLoading },
fallbackLogs,
} = useDynamicDataContext();

const tabs = useTabs();
Expand All @@ -36,7 +39,20 @@ export const PageView = () => {

const renderPageContent = React.useCallback(() => {
if (pipelineRun.error) {
return <ErrorContent error={pipelineRun.error} />;
return (
<Stack spacing={1}>
<ErrorContent error={pipelineRun.error} />
<LoadingWrapper isLoading={fallbackLogs.isLoading}>
<Stack spacing={2}>
<LogViewer
downloadName={`fallback-logs-${name}.log`}
logs={(fallbackLogs.data?.hits?.hits || []).map((el) => `${el._source.log}\n`)}
topActions={[<Typography variant="h6">Reserve Logs</Typography>]}
/>
</Stack>
</LoadingWrapper>
</Stack>
);
}

return (
Expand All @@ -51,6 +67,9 @@ export const PageView = () => {
tabs,
activeTab,
handleChangeTab,
fallbackLogs.isLoading,
fallbackLogs.data?.hits?.hits,
name,
]);

return (
Expand Down
18 changes: 16 additions & 2 deletions src/services/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
}
Loading