From e44299e78fafa14a83df582aa6ae4209e379eeb3 Mon Sep 17 00:00:00 2001 From: Karthik Jeeyar Date: Tue, 13 Feb 2024 12:53:48 +0530 Subject: [PATCH] feat(pipelines): add tekton core type and Output component integration utils (#16) * add tekton core types * add Integration utils and refactor * Add unit test for integration utils --- .changeset/thick-emus-perform.md | 6 + packages/pipelines/Readme.md | 36 +- packages/pipelines/package.json | 4 +- .../pipelines/src/__fixtures__/pipelinerun.ts | 115 ++++++ .../pipelines/src/__fixtures__/taskruns.ts | 85 +++++ .../Output/{OutputTab.tsx => Output.tsx} | 25 +- .../{OutputTabCard.tsx => OutputCard.tsx} | 6 +- .../Output/Tabs/Others/ResultsList.tsx | 3 +- .../Others/__tests__/ResultsList.test.tsx | 13 +- .../{OutputTab.test.tsx => Output.test.tsx} | 25 +- ...utTabCard.test.tsx => OutputCard.test.tsx} | 6 +- .../pipelines/src/components/Output/data.ts | 14 + .../__tests__/usePipelineRunOutput.test.ts | 65 ++++ .../__tests__/usePodContainerLogs.test.ts | 110 ++++++ .../src/components/Output/hooks/index.ts | 1 + .../Output/hooks/usePipelineRunOutput.ts | 114 ++++++ .../Output/hooks/usePodContainerLogs.ts | 46 +++ .../pipelines/src/components/Output/index.ts | 2 +- .../pipelines/src/components/Output/types.ts | 62 ++++ .../__tests__/data-transformer-utils.test.ts | 159 ++++++++ .../Output/utils/data-transformer-utils.ts | 139 +++++++ packages/pipelines/src/components/index.ts | 1 + packages/pipelines/src/index.ts | 2 + packages/pipelines/src/types/coreTekton.ts | 187 ++++++++++ packages/pipelines/src/types/index.ts | 5 + packages/pipelines/src/types/k8s.ts | 98 +++++ packages/pipelines/src/types/pipeline.ts | 66 ++++ packages/pipelines/src/types/pipelinerun.ts | 158 ++++++++ packages/pipelines/src/types/task.ts | 6 + packages/pipelines/src/types/taskrun.ts | 44 +++ .../utils/__tests__/pipelinerun-utils.test.ts | 62 ++++ packages/pipelines/src/utils/data-utils.ts | 348 ++++++++++++++++++ packages/pipelines/src/utils/index.ts | 1 + .../pipelines/src/utils/pipelinerun-utils.ts | 91 +++++ yarn.lock | 5 + 35 files changed, 2074 insertions(+), 36 deletions(-) create mode 100644 .changeset/thick-emus-perform.md create mode 100644 packages/pipelines/src/__fixtures__/pipelinerun.ts create mode 100644 packages/pipelines/src/__fixtures__/taskruns.ts rename packages/pipelines/src/components/Output/{OutputTab.tsx => Output.tsx} (85%) rename packages/pipelines/src/components/Output/{OutputTabCard.tsx => OutputCard.tsx} (88%) rename packages/pipelines/src/components/Output/__tests__/{OutputTab.test.tsx => Output.test.tsx} (86%) rename packages/pipelines/src/components/Output/__tests__/{OutputTabCard.test.tsx => OutputCard.test.tsx} (71%) create mode 100644 packages/pipelines/src/components/Output/hooks/__tests__/usePipelineRunOutput.test.ts create mode 100644 packages/pipelines/src/components/Output/hooks/__tests__/usePodContainerLogs.test.ts create mode 100644 packages/pipelines/src/components/Output/hooks/usePipelineRunOutput.ts create mode 100644 packages/pipelines/src/components/Output/hooks/usePodContainerLogs.ts create mode 100644 packages/pipelines/src/components/Output/utils/__tests__/data-transformer-utils.test.ts create mode 100644 packages/pipelines/src/components/Output/utils/data-transformer-utils.ts create mode 100644 packages/pipelines/src/types/coreTekton.ts create mode 100644 packages/pipelines/src/types/index.ts create mode 100644 packages/pipelines/src/types/k8s.ts create mode 100644 packages/pipelines/src/types/pipeline.ts create mode 100644 packages/pipelines/src/types/pipelinerun.ts create mode 100644 packages/pipelines/src/types/task.ts create mode 100644 packages/pipelines/src/types/taskrun.ts create mode 100644 packages/pipelines/src/utils/__tests__/pipelinerun-utils.test.ts create mode 100644 packages/pipelines/src/utils/data-utils.ts create mode 100644 packages/pipelines/src/utils/pipelinerun-utils.ts diff --git a/.changeset/thick-emus-perform.md b/.changeset/thick-emus-perform.md new file mode 100644 index 0000000..81e42cf --- /dev/null +++ b/.changeset/thick-emus-perform.md @@ -0,0 +1,6 @@ +--- +"@aonic-ui/pipelines": minor +--- + +Added core tekton types +Added integration utils, hooks and sample data helpers diff --git a/packages/pipelines/Readme.md b/packages/pipelines/Readme.md index eb78015..0b7628a 100644 --- a/packages/pipelines/Readme.md +++ b/packages/pipelines/Readme.md @@ -18,13 +18,43 @@ npm install @aonic-ui/pipelines ### Usage +Basic ```bash -import { OutputTab } from '@aonic-ui/pipelines'; +import { Output, usePipelineRunOutput } from '@aonic-ui/pipelines'; -// Example usage of OutputTab component +// Example usage of Output component - ``` + +Using helper functions +```bash +import { Output, usePipelineRunOutput } from '@aonic-ui/pipelines'; + + const output = usePipelineRunOutput( + mockData.pipelineRun as PipelineRunKind, + mockData.taskRuns, + getLogs); + + const getLogs = (podName, containerName): Promise => { + + // fetching the pod logs code goes here. + + return Promise.resolve('logs...') + } + + return ( + + ) +``` diff --git a/packages/pipelines/package.json b/packages/pipelines/package.json index cde489d..3de03a1 100644 --- a/packages/pipelines/package.json +++ b/packages/pipelines/package.json @@ -34,7 +34,8 @@ }, "homepage": "https://github.com/redhat-developer/aonic-ui#readme", "dependencies": { - "@patternfly/react-table": "^5.1.2" + "@patternfly/react-table": "^5.1.2", + "js-yaml": "^4.1.0" }, "devDependencies": { "@aonic-ui/eslint-config": "*", @@ -48,6 +49,7 @@ "@patternfly/react-tokens": "^5.1.2", "@types/jest": "^29.5.11", "@types/react": "^18.2.47", + "@types/js-yaml": "^4.0.5", "jest": "^29.7.0", "react": "^18.2.0", "rollup": "^4.9.5", diff --git a/packages/pipelines/src/__fixtures__/pipelinerun.ts b/packages/pipelines/src/__fixtures__/pipelinerun.ts new file mode 100644 index 0000000..9d88dc7 --- /dev/null +++ b/packages/pipelines/src/__fixtures__/pipelinerun.ts @@ -0,0 +1,115 @@ +import { PipelineRunKind } from '../types/pipelinerun'; +import { createPipelineRunData, mockPipelineRunConfig } from '../utils/data-utils'; +import { RunStatus, SucceedConditionReason } from '../utils/pipelinerun-utils'; + +const pipelineRunConfig: mockPipelineRunConfig = { + name: 'test', + status: RunStatus.Running, + tasks: [ + { name: 'scan-task', status: RunStatus.Succeeded }, + { name: 'sbom-task', status: RunStatus.Succeeded }, + ], +}; + +const { pipelineRun, taskRuns, pods } = createPipelineRunData(pipelineRunConfig); + +export const scanPipelineRun = pipelineRun; +export const scanTaskRuns = taskRuns; +export const scanTaskRunPods = pods; + +export enum DataState { + RUNNING = 'Running', + SUCCEEDED = 'Succeeded', + FAILED = 'Failed', + SKIPPED = 'Skipped', + PENDING = 'PipelineRunPending', + STOPPED = 'StoppedRunFinally', + CANCELLED = 'CancelledRunFinally', + CANCELLING = 'PipelineRunCancelling', + STOPPING = 'PipelineRunStopping', + + /*Custom data state to test various scnearios*/ + STATUS_WITHOUT_CONDITIONS = 'StatusWithoutCondition', + EXCEEDED_NODE_RESOURCES = 'ExceededNodeResources', + STATUS_WITH_EMPTY_CONDITIONS = 'StatusWithEmptyCondition', + STATUS_WITH_UNKNOWN_REASON = 'StatusWithUnknownReason', +} + +type TestPipelineRuns = { [key in Partial]: PipelineRunKind }; + +const sampleTasks = [ + { name: 'build-task', status: RunStatus.Succeeded }, + { name: 'scan-task', status: RunStatus.Succeeded }, + { name: 'sbom-task', status: RunStatus.Succeeded }, +]; + +export const testPipelineRuns: TestPipelineRuns = { + [DataState.RUNNING]: createPipelineRunData({ + name: 'test-plr-running', + status: RunStatus.Running, + tasks: sampleTasks, + }).pipelineRun, + [DataState.PENDING]: createPipelineRunData({ + name: 'test-plr-pending', + status: RunStatus.Pending, + tasks: sampleTasks, + }).pipelineRun, + [DataState.SUCCEEDED]: createPipelineRunData({ + name: 'test-plr-succeeded', + status: RunStatus.Succeeded, + tasks: sampleTasks, + }).pipelineRun, + [DataState.FAILED]: createPipelineRunData({ + name: 'test-plr-failed', + status: RunStatus.Failed, + tasks: sampleTasks, + }).pipelineRun, + + [DataState.CANCELLING]: createPipelineRunData({ + name: 'test-plr-cancelling', + status: RunStatus.Running, + spec: { status: 'CancelledRunFinally' }, + tasks: sampleTasks, + }).pipelineRun, + [DataState.CANCELLED]: createPipelineRunData({ + name: 'test-plr-cancelled', + status: RunStatus.Cancelled, + spec: { status: 'CancelledRunFinally' }, + tasks: sampleTasks, + }).pipelineRun, + [DataState.STOPPING]: createPipelineRunData({ + name: 'test-plr-cancelled', + status: SucceedConditionReason.PipelineRunStopping, + tasks: sampleTasks, + }).pipelineRun, + [DataState.SKIPPED]: createPipelineRunData({ + name: 'test-plr-skipped', + status: RunStatus.Skipped, + tasks: sampleTasks, + }).pipelineRun, + [DataState.STOPPED]: createPipelineRunData({ + name: 'test-plr-stopping', + status: SucceedConditionReason.PipelineRunStopped, + tasks: sampleTasks, + }).pipelineRun, + [DataState.EXCEEDED_NODE_RESOURCES]: createPipelineRunData({ + name: 'test-plr-exceeded-node-resources', + status: SucceedConditionReason.ExceededNodeResources, + tasks: sampleTasks, + }).pipelineRun, + [DataState.STATUS_WITHOUT_CONDITIONS]: createPipelineRunData({ + name: 'test-plr-without-conditions', + status: 'STATUS_WITHOUT_CONDITIONS', + tasks: sampleTasks, + }).pipelineRun, + [DataState.STATUS_WITH_UNKNOWN_REASON]: createPipelineRunData({ + name: 'test-plr-with-unknown-reason', + status: RunStatus.Unknown, + tasks: sampleTasks, + }).pipelineRun, + [DataState.STATUS_WITH_EMPTY_CONDITIONS]: createPipelineRunData({ + name: 'test-plr-with-empty-conditions', + status: 'STATUS_WITH_EMPTY_CONDITIONS', + tasks: sampleTasks, + }).pipelineRun, +}; diff --git a/packages/pipelines/src/__fixtures__/taskruns.ts b/packages/pipelines/src/__fixtures__/taskruns.ts new file mode 100644 index 0000000..fad75a7 --- /dev/null +++ b/packages/pipelines/src/__fixtures__/taskruns.ts @@ -0,0 +1,85 @@ +import { createPipelineRunData, mockPipelineRunConfig } from '../utils/data-utils'; +import { RunStatus } from '../utils/pipelinerun-utils'; + +const pipelineRunConfig: mockPipelineRunConfig = { + name: 'test-plr', + status: RunStatus.Running, + tasks: [ + { + name: 'sbom-task', + status: RunStatus.Succeeded, + annotations: { + 'task.output.location': 'results', + 'task.results.format': 'application/text', + 'task.results.key': 'LINK_TO_SBOM', + }, + }, + { + name: 'ec-task', + status: RunStatus.Succeeded, + annotations: { + 'task.results.type': 'ec', + 'task.results.format': 'application/json', + 'task.output.location': 'logs', + 'task.output.container': 'step-report', + }, + }, + { + name: 'acs-image-scan-task', + status: RunStatus.Succeeded, + annotations: { + 'task.results.type': 'roxctl-image-scan', + 'task.results.format': 'application/json', + 'task.output.location': 'logs', + 'task.output.container': 'step-report', + }, + }, + { + name: 'acs-image-check-task', + status: RunStatus.Succeeded, + annotations: { + 'task.results.type': 'roxctl-image-check', + 'task.results.format': 'application/json', + 'task.output.location': 'logs', + 'task.output.container': 'step-report', + }, + }, + { + name: 'acs-deployment-check-task', + status: RunStatus.Succeeded, + annotations: { + 'task.results.type': 'roxctl-deployment-check', + 'task.results.format': 'application/json', + 'task.output.location': 'logs', + 'task.output.container': 'step-report', + }, + }, + { + name: 'sbom-with-external-link-task', + status: RunStatus.Succeeded, + annotations: { + 'task.output.location': 'results', + 'task.results.type': 'external-link', + 'task.results.format': 'application/text', + 'task.results.key': 'LINK_TO_SBOM', + }, + results: [ + { + name: 'LINK_TO_SBOM', + type: 'string', + value: 'http://quay.io/test/image:build-8e536-1692702836', + }, + ], + }, + ], + createTaskRuns: true, + createPods: true, +}; + +const { pipelineRun, taskRuns, pods } = createPipelineRunData(pipelineRunConfig); + +export const SampleOutputPipelineRunData = { + pipelineRun, + taskRuns, + pods, +}; diff --git a/packages/pipelines/src/components/Output/OutputTab.tsx b/packages/pipelines/src/components/Output/Output.tsx similarity index 85% rename from packages/pipelines/src/components/Output/OutputTab.tsx rename to packages/pipelines/src/components/Output/Output.tsx index dbb36d0..4b0e46b 100644 --- a/packages/pipelines/src/components/Output/OutputTab.tsx +++ b/packages/pipelines/src/components/Output/Output.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import OutputTabCard from './OutputTabCard'; +import OutputCard from './OutputCard'; import AdvancedClusterSecurity from './Tabs/AdvancedClusterSecurity/AdvancedClusterSecurity'; import EnterpriseContract from './Tabs/EnterpriseContract/EnterpriseContract'; import ResultsList, { ResultsListProps } from './Tabs/Others/ResultsList'; @@ -10,14 +10,19 @@ import { getEnterpriseContractStatus } from './utils/ec-utils'; import { isEmpty } from './utils/helper-utils'; import { getACStatusLabel, getECStatusLabel } from './utils/summary-utils'; -type OutputTabProps = { +export type OutputProps = { enterpriseContractPolicies?: EnterpriseContractPolicy[]; acsImageScanResult?: ACSImageScanResult; acsImageCheckResults?: ACSCheckResults; acsDeploymentCheckResults?: ACSCheckResults; } & ResultsListProps; -const OutputTab: React.FC = ({ +/** + * Output component supports EC, ACS policy reports and pipelinerun results. + * @param OutputProps + * @returns React.ReactNode + */ +const Output: React.FC = ({ enterpriseContractPolicies = [], acsImageCheckResults = {} as ACSCheckResults, acsImageScanResult = {} as ACSImageScanResult, @@ -46,16 +51,16 @@ const OutputTab: React.FC = ({ return ( <> {showECCard && ( - - + )} {showACSCard && ( - = ({ acsImageCheckResults={acsImageCheckResults} acsDeploymentCheckResults={acsDeploymentCheckResults} /> - + )} {results.length > 0 && showOnlyResults ? ( ) : results.length > 0 ? ( - + - + ) : null} ); }; -export default OutputTab; +export default Output; diff --git a/packages/pipelines/src/components/Output/OutputTabCard.tsx b/packages/pipelines/src/components/Output/OutputCard.tsx similarity index 88% rename from packages/pipelines/src/components/Output/OutputTabCard.tsx rename to packages/pipelines/src/components/Output/OutputCard.tsx index 7f5391a..3518ff9 100644 --- a/packages/pipelines/src/components/Output/OutputTabCard.tsx +++ b/packages/pipelines/src/components/Output/OutputCard.tsx @@ -9,13 +9,13 @@ import { FlexItem, } from '@patternfly/react-core'; -type OutputTabCardProps = { +type OutputCardProps = { title: string; isOpen?: boolean; badge?: React.ReactNode; children: React.ReactNode; }; -const OutputTabCard: React.FC = ({ title, badge, isOpen, children }) => { +const OutputCard: React.FC = ({ title, badge, isOpen, children }) => { const [tabOpen, setTabOpen] = React.useState(isOpen ?? false); const id = title?.replace(/\//g, '-')?.toLowerCase(); @@ -44,4 +44,4 @@ const OutputTabCard: React.FC = ({ title, badge, isOpen, chi ); }; -export default OutputTabCard; +export default OutputCard; diff --git a/packages/pipelines/src/components/Output/Tabs/Others/ResultsList.tsx b/packages/pipelines/src/components/Output/Tabs/Others/ResultsList.tsx index 2801333..5a31c3f 100644 --- a/packages/pipelines/src/components/Output/Tabs/Others/ResultsList.tsx +++ b/packages/pipelines/src/components/Output/Tabs/Others/ResultsList.tsx @@ -1,5 +1,6 @@ import { Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant } from '@patternfly/react-core'; import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { RunStatus } from '../../../../utils/pipelinerun-utils'; import { handleURLs } from './HandleUrls'; export interface ResultsListProps { @@ -8,7 +9,7 @@ export interface ResultsListProps { value: string; }[]; pipelineRunName: string; - pipelineRunStatus: string; + pipelineRunStatus: RunStatus; } const ResultsList: React.FC = ({ diff --git a/packages/pipelines/src/components/Output/Tabs/Others/__tests__/ResultsList.test.tsx b/packages/pipelines/src/components/Output/Tabs/Others/__tests__/ResultsList.test.tsx index d3193ae..e9ac104 100644 --- a/packages/pipelines/src/components/Output/Tabs/Others/__tests__/ResultsList.test.tsx +++ b/packages/pipelines/src/components/Output/Tabs/Others/__tests__/ResultsList.test.tsx @@ -1,11 +1,16 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; +import { RunStatus } from '../../../../../utils/pipelinerun-utils'; import ResultsList from '../ResultsList'; describe('ResultsList', () => { test('should return null', () => { render( - , + , ); expect(screen.queryByTestId('results-table')).not.toBeInTheDocument(); }); @@ -15,7 +20,7 @@ describe('ResultsList', () => { , ); expect(screen.queryByTestId('results-table')).toBeInTheDocument(); @@ -26,7 +31,7 @@ describe('ResultsList', () => { , ); screen.getByRole('link', { name: 'result link' }); @@ -37,7 +42,7 @@ describe('ResultsList', () => { , ); screen.getByText('pipelinerun-1 results not available due to failure'); diff --git a/packages/pipelines/src/components/Output/__tests__/OutputTab.test.tsx b/packages/pipelines/src/components/Output/__tests__/Output.test.tsx similarity index 86% rename from packages/pipelines/src/components/Output/__tests__/OutputTab.test.tsx rename to packages/pipelines/src/components/Output/__tests__/Output.test.tsx index a704b0d..90f4b67 100644 --- a/packages/pipelines/src/components/Output/__tests__/OutputTab.test.tsx +++ b/packages/pipelines/src/components/Output/__tests__/Output.test.tsx @@ -1,21 +1,22 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; +import { RunStatus } from '../../../utils/pipelinerun-utils'; import { acsDeploymentCheck, acsImageCheckResults, acsImageScanResult, mockEnterpriseContractUIData, } from '../data'; -import OutputTab from '../OutputTab'; +import Output from '../Output'; describe('OutputTab', () => { test('should render OutputTab with enterprise contract', () => { render( - , ); @@ -28,11 +29,11 @@ describe('OutputTab', () => { test('should render OutputTab with adavanced cluster security data', () => { render( - , ); @@ -43,7 +44,7 @@ describe('OutputTab', () => { test('should not render issues found badge in adavanced cluster security tab', () => { render( - { }} results={[{ name: 'result-name', value: 'result-value' }]} pipelineRunName="pipelinerun-1" - pipelineRunStatus="Succeeded" + pipelineRunStatus={RunStatus.Succeeded} />, ); @@ -96,17 +97,21 @@ describe('OutputTab', () => { test('should not render OutputTab with results section', () => { render( - , + , ); expect(screen.queryByTestId('results-table')).not.toBeInTheDocument(); }); test('should render OutputTab with results section', () => { render( - , ); screen.getByText('result-value'); diff --git a/packages/pipelines/src/components/Output/__tests__/OutputTabCard.test.tsx b/packages/pipelines/src/components/Output/__tests__/OutputCard.test.tsx similarity index 71% rename from packages/pipelines/src/components/Output/__tests__/OutputTabCard.test.tsx rename to packages/pipelines/src/components/Output/__tests__/OutputCard.test.tsx index eb6d244..4fb831c 100644 --- a/packages/pipelines/src/components/Output/__tests__/OutputTabCard.test.tsx +++ b/packages/pipelines/src/components/Output/__tests__/OutputCard.test.tsx @@ -1,16 +1,16 @@ import '@testing-library/jest-dom'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import OutputTabCard from '../OutputTabCard'; +import OutputCard from '../OutputCard'; describe('OutputCard', () => { test('should render output card', () => { - render(Card content); + render(Card content); screen.getByText('Output card'); }); test('should render output card', () => { - render(Card content); + render(Card content); screen.getByText('Output card'); diff --git a/packages/pipelines/src/components/Output/data.ts b/packages/pipelines/src/components/Output/data.ts index 4685fad..1b03001 100644 --- a/packages/pipelines/src/components/Output/data.ts +++ b/packages/pipelines/src/components/Output/data.ts @@ -163,6 +163,20 @@ export const mockEnterpriseContractJSON: EnterpriseContractResult = { msg: 'CVE scan results not found', }, ], + warnings: [ + { + metadata: { + code: 'cve.missing_cve_scan_results', + collections: ['minimal'], + description: + 'The clair-scan task results have not been found in the SLSA Provenance attestation of the build pipeline.', + ['effective_on']: '2022-01-01T00:00:00Z', + title: 'Missing CVE scan results', + solution: 'solution for failure', + }, + msg: 'CVE scan results not found', + }, + ], }, ], key: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWVUppvU1x8t866CQQSXbKpojoaTq\nimMnVnZ31e2ubZHKL1LdfgPG2gHIPeSeouTa8upOz9W+xxBFnA0X515Nsw==\n-----END PUBLIC KEY-----\n', diff --git a/packages/pipelines/src/components/Output/hooks/__tests__/usePipelineRunOutput.test.ts b/packages/pipelines/src/components/Output/hooks/__tests__/usePipelineRunOutput.test.ts new file mode 100644 index 0000000..c6dcee6 --- /dev/null +++ b/packages/pipelines/src/components/Output/hooks/__tests__/usePipelineRunOutput.test.ts @@ -0,0 +1,65 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { SampleOutputPipelineRunData } from '../../../../__fixtures__/taskruns'; +import { RunStatus } from '../../../../utils/pipelinerun-utils'; +import { + acsDeploymentCheck, + acsImageCheckResults, + acsImageScanResult, + mockEnterpriseContractJSON, +} from '../../data'; +import { usePipelineRunOutput } from '../usePipelineRunOutput'; + +const { pipelineRun, taskRuns = [] } = SampleOutputPipelineRunData; +describe('usePipelineRunOutput', () => { + test('should return default value', async () => { + const { result } = renderHook(() => + usePipelineRunOutput( + pipelineRun, + taskRuns, + jest.fn(() => Promise.resolve('')), + ), + ); + + await waitFor(() => { + const { results, status, ec, acsImageScan, acsImageCheck, acsDeploymentCheck } = + result.current; + + expect(status).toBe(RunStatus.Running); + expect(results.loading).toBe(true); + expect(ec?.data).toHaveLength(0); + expect(acsImageScan?.data).toHaveLength(0); + expect(acsImageCheck?.data).toHaveLength(0); + expect(acsDeploymentCheck?.data).toHaveLength(0); + }); + }); + + test('should return actual data', async () => { + const getLogs = (podName: string) => { + let data: string = ''; + + if (podName.includes('ec-task')) { + data = JSON.stringify(mockEnterpriseContractJSON); + } else if (podName.includes('image-scan')) { + data = JSON.stringify(acsImageScanResult); + } else if (podName.includes('image-check')) { + data = JSON.stringify(acsImageCheckResults); + } else if (podName.includes('deployment-check')) { + data = JSON.stringify(acsDeploymentCheck); + } + return Promise.resolve(data); + }; + const { result } = renderHook(() => usePipelineRunOutput(pipelineRun, taskRuns, getLogs)); + + await waitFor(() => { + const { results, status, ec, acsImageScan, acsImageCheck, acsDeploymentCheck } = + result.current; + + expect(status).toBe(RunStatus.Running); + expect(results.loading).toBe(true); + expect(ec?.data).toHaveLength(3); + expect(acsImageScan?.data?.result.vulnerabilities).toHaveLength(154); + expect(acsImageCheck?.data?.results).toHaveLength(1); + expect(acsDeploymentCheck?.data?.results).toHaveLength(1); + }); + }); +}); diff --git a/packages/pipelines/src/components/Output/hooks/__tests__/usePodContainerLogs.test.ts b/packages/pipelines/src/components/Output/hooks/__tests__/usePodContainerLogs.test.ts new file mode 100644 index 0000000..7270b61 --- /dev/null +++ b/packages/pipelines/src/components/Output/hooks/__tests__/usePodContainerLogs.test.ts @@ -0,0 +1,110 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { SampleOutputPipelineRunData } from '../../../../__fixtures__/taskruns'; +import { TaskRunKind } from '../../../../types'; +import { mockEnterpriseContractJSON } from '../../data'; +import { usePodContainerLogs } from '../usePodContainerLogs'; + +const [sbomTaskRun, ecTaskRun] = SampleOutputPipelineRunData.taskRuns ?? []; +describe('usePodContainerLogs', () => { + test('should return the default value and loading state', async () => { + const { result } = renderHook(() => + usePodContainerLogs( + sbomTaskRun, + jest.fn(() => Promise.resolve('')), + ), + ); + await waitFor(() => { + const { data, loading } = result.current; + + expect(data).toBe(''); + expect(loading).toBe(true); + }); + }); + + test('should return the default value and loading state', async () => { + const getLogs = (): Promise => + new Promise((resolve) => resolve(JSON.stringify(mockEnterpriseContractJSON))); + + const { result } = renderHook(() => usePodContainerLogs(ecTaskRun, getLogs)); + + await waitFor(() => { + const { data, loading } = result.current; + expect(JSON.parse(data).components).toHaveLength(3); + expect(loading).toBe(false); + }); + }); + + test('should call the getLogs function with podName and containerName', async () => { + const logsFetcher = jest.fn(() => Promise.resolve('')); + const { result } = renderHook(() => usePodContainerLogs(ecTaskRun, logsFetcher)); + + await waitFor(() => { + const { data, loading } = result.current; + + expect(logsFetcher).toHaveBeenCalledWith('test-plr-ec-task-pod', 'step-report'); + expect(data).toBe(''); + expect(loading).toBe(true); + }); + }); + test('should handle the error when non-promise function is passed as getLogs', async () => { + const errorFunction = jest.fn(); + jest.spyOn(console, 'error').mockImplementation(errorFunction); + + const logsFetcher = jest.fn(); + renderHook(() => usePodContainerLogs(ecTaskRun, logsFetcher)); + await waitFor(() => { + expect(errorFunction).toHaveBeenCalledWith( + 'Something went wrong while fetching logs', + expect.anything(), + ); + }); + }); + + test('should handle the error when getLogs errors out', async () => { + const errorFunction = jest.fn(); + jest.spyOn(console, 'warn').mockClear().mockImplementation(errorFunction); + + const logsFetcher = jest.fn(() => Promise.reject('failed to fetch')); + renderHook(() => usePodContainerLogs(ecTaskRun, logsFetcher)); + await waitFor(() => { + expect(errorFunction).toHaveBeenCalledWith( + 'Error while fetching data from pod logs', + 'failed to fetch', + ); + }); + }); + + test('should handle the invalid taskrun', async () => { + const logsFetcher = jest.fn(() => Promise.resolve('')); + const { result } = renderHook(() => usePodContainerLogs({} as TaskRunKind, logsFetcher)); + await waitFor(() => { + const { data, loading } = result.current; + expect(data).toBe(''); + expect(loading).toBe(true); + }); + }); + + test('should not call setState hooks when component is unmounted', async () => { + const logsFetcher = jest.fn(() => Promise.resolve(JSON.stringify(mockEnterpriseContractJSON))); + const { result, unmount } = renderHook(() => usePodContainerLogs(ecTaskRun, logsFetcher)); + unmount(); + + await waitFor(() => { + const { data, loading } = result.current; + expect(data).toBe(''); + expect(loading).toBe(true); + }); + }); + + test('should not throw errors when component is unmounted', async () => { + const errorFunction = jest.fn(); + jest.spyOn(console, 'warn').mockClear().mockImplementation(errorFunction); + + const logsFetcher = jest.fn(() => Promise.reject('failed to fetch')); + const { unmount } = renderHook(() => usePodContainerLogs(ecTaskRun, logsFetcher)); + unmount(); + await waitFor(() => { + expect(errorFunction).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/packages/pipelines/src/components/Output/hooks/index.ts b/packages/pipelines/src/components/Output/hooks/index.ts index e69de29..87c5e37 100644 --- a/packages/pipelines/src/components/Output/hooks/index.ts +++ b/packages/pipelines/src/components/Output/hooks/index.ts @@ -0,0 +1 @@ +export * from './usePipelineRunOutput'; diff --git a/packages/pipelines/src/components/Output/hooks/usePipelineRunOutput.ts b/packages/pipelines/src/components/Output/hooks/usePipelineRunOutput.ts new file mode 100644 index 0000000..762b31e --- /dev/null +++ b/packages/pipelines/src/components/Output/hooks/usePipelineRunOutput.ts @@ -0,0 +1,114 @@ +import React from 'react'; +import { TektonResultsRun } from '../../../types/coreTekton'; +import { PipelineRunKind } from '../../../types/pipelinerun'; +import { TaskRunKind } from '../../../types/taskrun'; +import { pipelineRunStatus, RunStatus } from '../../../utils/pipelinerun-utils'; +import { + EnterpriseContractResult, + OutputGroup, + TaskRunResultsAnnotations, + TaskRunResultsFormatValue, +} from '../types'; +import { + formatData, + getTaskrunsOutputGroup, + mapEnterpriseContractResultData, +} from '../utils/data-transformer-utils'; +import { usePodContainerLogs } from './usePodContainerLogs'; + +/** + * Returns the data needed to pass into the Output component + * @param pipelineRun + * @param taskRuns + * @param getLogs + * @returns OutputGroup + */ +export const usePipelineRunOutput = ( + pipelineRun: PipelineRunKind, + taskRuns: TaskRunKind[], + getLogs: (podName: string, containerName: string) => Promise, +): OutputGroup => { + const { acsImageScanTaskRun, acsImageCheckTaskRun, acsDeploymentCheckTaskRun, ecTaskRun } = + getTaskrunsOutputGroup(pipelineRun?.metadata?.name, taskRuns); + + const getTaskRunFormat = (obj: TaskRunKind | undefined): string => + obj?.metadata?.annotations?.[TaskRunResultsAnnotations.FORMAT] ?? + TaskRunResultsFormatValue.TEXT; + + const { data: ecValue, loading: ecLoading } = usePodContainerLogs(ecTaskRun, getLogs); + const { data: acsImageScanValue, loading: acsImageScanLoading } = usePodContainerLogs( + acsImageScanTaskRun, + getLogs, + ); + const { data: acsValue, loading: acsImageCheckLoading } = usePodContainerLogs( + acsImageCheckTaskRun, + getLogs, + ); + const { data: acsDcValue, loading: acsDeploymentCheckLoading } = usePodContainerLogs( + acsDeploymentCheckTaskRun, + getLogs, + ); + + const ecTaskFormat = getTaskRunFormat(ecTaskRun); + const acsImageScanFormat = getTaskRunFormat(acsImageScanTaskRun); + const acsImageCheckFormat = getTaskRunFormat(acsImageCheckTaskRun); + const acsDeploymentCheckFormat = getTaskRunFormat(acsDeploymentCheckTaskRun); + + const acsImageScanData = formatData(acsImageScanFormat, acsImageScanValue ?? ''); + const acsImageCheckData = formatData(acsImageCheckFormat, acsValue ?? ''); + const acsDeploymentCheckData = formatData(acsDeploymentCheckFormat, acsDcValue ?? ''); + + const ecData = mapEnterpriseContractResultData( + formatData(ecTaskFormat, ecValue ?? ([] as any)) as EnterpriseContractResult, + ); + const status = pipelineRunStatus(pipelineRun); + const results = ((pipelineRun?.status?.pipelineResults || pipelineRun?.status?.results) ?? + []) as TektonResultsRun[]; + + return React.useMemo( + () => ({ + status, + results: { + loading: status !== RunStatus.Succeeded, + data: results, + }, + ec: { + data: ecData, + loading: ecLoading, + taskRun: ecTaskRun, + }, + acsImageScan: { + data: acsImageScanData, + loading: acsImageScanLoading, + taskRun: acsImageScanTaskRun, + }, + acsImageCheck: { + data: acsImageCheckData, + loading: acsImageCheckLoading, + taskRun: acsImageCheckTaskRun, + }, + acsDeploymentCheck: { + data: acsDeploymentCheckData, + loading: acsDeploymentCheckLoading, + taskRun: acsDeploymentCheckTaskRun, + }, + }), + [ + status, + ecData, + results, + ecTaskRun, + ecLoading, + pipelineRun, + acsImageScanData, + acsImageScanLoading, + acsImageScanTaskRun, + acsImageCheckData, + acsImageCheckLoading, + acsImageCheckTaskRun, + acsDeploymentCheckData, + acsDeploymentCheckLoading, + acsDeploymentCheckTaskRun, + ], + ); +}; diff --git a/packages/pipelines/src/components/Output/hooks/usePodContainerLogs.ts b/packages/pipelines/src/components/Output/hooks/usePodContainerLogs.ts new file mode 100644 index 0000000..1e8b11a --- /dev/null +++ b/packages/pipelines/src/components/Output/hooks/usePodContainerLogs.ts @@ -0,0 +1,46 @@ +import React from 'react'; +import { TaskRunKind } from '../../../types/taskrun'; +import { TaskRunResultsAnnotations } from '../types'; + +const getTaskRunContainer = (obj: TaskRunKind | undefined): string => + obj?.metadata?.annotations?.[TaskRunResultsAnnotations.CONTAINER] ?? 'step-report'; + +export const usePodContainerLogs = ( + taskRun: TaskRunKind | undefined, + getLogs: (podName: string, containerName: string) => Promise, +): { data: string; loading: boolean } => { + const [data, setData] = React.useState(''); + const [loading, setLoading] = React.useState(true); + + const podName = taskRun?.status?.podName; + const containerName = getTaskRunContainer(taskRun); + + React.useEffect(() => { + let unmount = false; + if (podName) { + try { + getLogs?.(podName, containerName) + .then((res) => { + if (unmount) return; + setData(res); + setLoading(false); + }) + .catch((err) => { + if (unmount) return; + + setLoading(false); + // eslint-disable-next-line no-console + console.warn('Error while fetching data from pod logs', err); + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Something went wrong while fetching logs', e); + } + } + return () => { + unmount = true; + }; + }, [getLogs, containerName, podName]); + + return { data, loading }; +}; diff --git a/packages/pipelines/src/components/Output/index.ts b/packages/pipelines/src/components/Output/index.ts index 51130d6..e22fe32 100644 --- a/packages/pipelines/src/components/Output/index.ts +++ b/packages/pipelines/src/components/Output/index.ts @@ -1,2 +1,2 @@ -export { default as Output } from './OutputTab'; +export { default as Output } from './Output'; export { EnterpriseContract, AdvancedClusterSecurity, ResultsList } from './Tabs'; diff --git a/packages/pipelines/src/components/Output/types.ts b/packages/pipelines/src/components/Output/types.ts index 1d2731c..464253b 100644 --- a/packages/pipelines/src/components/Output/types.ts +++ b/packages/pipelines/src/components/Output/types.ts @@ -1,5 +1,9 @@ // Enterprise constract types and enums. +import { TektonResultsRun } from '../../types/coreTekton'; +import { TaskRunKind } from '../../types/taskrun'; +import { RunStatus } from '../../utils/pipelinerun-utils'; + export enum ENTERPRISE_CONTRACT_POLICY_STATUS { failed = 'Failed', successes = 'Success', @@ -160,3 +164,61 @@ export interface ACSCheckResults { results: Result[]; summary: ACSImageCheckSummary; } + +export enum TaskRunResultsAnnotations { + KEY = 'task.results.key', + TYPE = 'task.results.type', + LOCATION = 'task.results.location', + CONTAINER = 'task.results.container', + FORMAT = 'task.results.format', +} + +export enum TaskRunResultsTypeValue { + EC = 'ec', + EXTERNAL_LINK = 'external-link', + ROXCTL_IMAGE_SCAN = 'roxctl-image-scan', + ROXCTL_IMAGE_CHECK = 'roxctl-image-check', + ROXCTL_DEPLOYMENT_CHECK = 'roxctl-deployment-check', +} + +export enum TaskRunResultsFormatValue { + JSON = 'application/json', + YAML = 'application/yaml', + TEXT = 'application/text', +} + +export enum TaskRunResultsLocationValue { + LOGS = 'logs', + RESULTS = 'results', +} + +export enum TaskRunResultsKeyValue { + SBOM = 'LINK_TO_SBOM', + SCAN_OUTPUT = 'SCAN_OUTPUT', +} + +export enum TaskType { + sbom = 'sbom', + ec = 'ec', + acsImageScan = 'acsImageScan', + acsImageCheck = 'acsImageCheck', + acsDeploymentCheck = 'acsDeploymentCheck', +} + +export type OutputGroup = { + [key in TaskType]?: { + taskRun: TaskRunKind | undefined; + loading: boolean; + data: string | object | any; + }; +} & { + status: RunStatus; + results: { + loading: boolean; + data: TektonResultsRun[] | []; + }; +}; + +export type OutputTaskRunGroup = { + [key in `${TaskType}TaskRun`]?: TaskRunKind; +}; diff --git a/packages/pipelines/src/components/Output/utils/__tests__/data-transformer-utils.test.ts b/packages/pipelines/src/components/Output/utils/__tests__/data-transformer-utils.test.ts new file mode 100644 index 0000000..e73378e --- /dev/null +++ b/packages/pipelines/src/components/Output/utils/__tests__/data-transformer-utils.test.ts @@ -0,0 +1,159 @@ +import { SampleOutputPipelineRunData } from '../../../../__fixtures__/taskruns'; +import { TaskRunKind } from '../../../../types'; +import { mockEnterpriseContractJSON } from '../../data'; +import { EnterpriseContractResult, TaskRunResultsFormatValue } from '../../types'; +import { + formatData, + getSbomLink, + getTaskrunsOutputGroup, + hasExternalLink, + isACSDeploymentCheckTaskRun, + isACSImageCheckTaskRun, + isACSImageScanTaskRun, + isECTaskRun, + isSbomTaskRun, + mapEnterpriseContractResultData, +} from '../data-transformer-utils'; + +const { pipelineRun, taskRuns = [] } = SampleOutputPipelineRunData; +const [ + sbomTaskrun, + ecTaskrun, + acsImageScanTaskrun, + acsImageCheckTaskrun, + acsDeploymentCheckTaskrun, + sbomTaskrunWithExternalLink, +] = taskRuns ?? []; +describe('isSbomTaskRun', () => { + test('should return false for invalid values', () => { + expect(isSbomTaskRun({} as TaskRunKind)).toBe(false); + }); + + test('should return true for value values', () => { + expect(isSbomTaskRun(sbomTaskrun)).toBe(true); + }); +}); + +describe('isECTaskRun', () => { + test('should return false for invalid values', () => { + expect(isECTaskRun({} as TaskRunKind)).toBe(false); + }); + + test('should return true for value values', () => { + expect(isECTaskRun(ecTaskrun)).toBe(true); + }); +}); + +describe('isACSImageScanTaskRun', () => { + test('should return false for invalid values', () => { + expect(isACSImageScanTaskRun({} as TaskRunKind)).toBe(false); + }); + + test('should return true for value values', () => { + expect(isACSImageScanTaskRun(acsImageScanTaskrun)).toBe(true); + }); +}); + +describe('isACSImageCheckTaskRun', () => { + test('should return false for invalid values', () => { + expect(isACSImageCheckTaskRun({} as TaskRunKind)).toBe(false); + }); + + test('should return true for value values', () => { + expect(isACSImageCheckTaskRun(acsImageCheckTaskrun)).toBe(true); + }); +}); + +describe('isACSDeploymentCheckTaskRun', () => { + test('should return false for invalid values', () => { + expect(isACSDeploymentCheckTaskRun({} as TaskRunKind)).toBe(false); + }); + test('should return true for value values', () => { + expect(isACSDeploymentCheckTaskRun(acsDeploymentCheckTaskrun)).toBe(true); + }); +}); + +describe('getTaskrunsOutputGroup', () => { + test('should not return the output group taskruns for invalid pipelinerun', () => { + const outputGroup = getTaskrunsOutputGroup('invalid-pipelinerun', taskRuns); + + expect(outputGroup.sbomTaskRun).toBeUndefined(); + expect(outputGroup.ecTaskRun).toBeUndefined(); + expect(outputGroup.acsImageScanTaskRun).toBeUndefined(); + expect(outputGroup.acsImageCheckTaskRun).toBeUndefined(); + expect(outputGroup.acsDeploymentCheckTaskRun).toBeUndefined(); + }); + + test('should return be all the output group taskruns', () => { + const outputGroup = getTaskrunsOutputGroup(pipelineRun.metadata?.name, taskRuns); + + expect(outputGroup.sbomTaskRun).toBeDefined(); + expect(outputGroup.ecTaskRun).toBeDefined(); + expect(outputGroup.acsImageScanTaskRun).toBeDefined(); + expect(outputGroup.acsImageCheckTaskRun).toBeDefined(); + expect(outputGroup.acsDeploymentCheckTaskRun).toBeDefined(); + }); +}); + +describe('hasExternalLink', () => { + test('should return false for internal sbomTaskrun', () => { + expect(hasExternalLink(sbomTaskrun)).toBe(false); + }); + test('should return true for external sbomTaskrun', () => { + expect(hasExternalLink(sbomTaskrunWithExternalLink)).toBe(true); + }); +}); + +describe('getSbomLink', () => { + test('should return undefined if no link is set', () => { + expect(getSbomLink(sbomTaskrun)).toBeUndefined(); + }); + test('should return the sbom link', () => { + expect(getSbomLink(sbomTaskrunWithExternalLink)).toBe( + 'http://quay.io/test/image:build-8e536-1692702836', + ); + }); +}); +describe('formatData', () => { + test('should format the application/json data', () => { + const json = { apiVersion: 'v1', kind: 'Test', metadata: { name: 'test' } }; + + expect(formatData(TaskRunResultsFormatValue.JSON, JSON.stringify(json))).toMatchObject( + expect.objectContaining({ + kind: 'Test', + }), + ); + }); + + test('should format the application/yaml data', () => { + const yaml = `apiVersion: v1 +kind: Test +metdata: + name: test`; + + expect(formatData(TaskRunResultsFormatValue.YAML, yaml)).toMatchObject( + expect.objectContaining({ + kind: 'Test', + }), + ); + }); + + test('should format the application/text data', () => { + const text = `text output data`; + expect(formatData(TaskRunResultsFormatValue.TEXT, text)).toBe(''); + }); + + test('should return empty string for unsupported type', () => { + const text = `text output data`; + expect(formatData('application/unsupported', text)).toBe(''); + }); +}); + +describe('mapEnterpriseContractResultData', () => { + test('should process the information and return the array', () => { + expect(mapEnterpriseContractResultData({} as EnterpriseContractResult)).toHaveLength(0); + }); + test('should process the information and return the array', () => { + expect(mapEnterpriseContractResultData(mockEnterpriseContractJSON)).toHaveLength(3); + }); +}); diff --git a/packages/pipelines/src/components/Output/utils/data-transformer-utils.ts b/packages/pipelines/src/components/Output/utils/data-transformer-utils.ts new file mode 100644 index 0000000..af50266 --- /dev/null +++ b/packages/pipelines/src/components/Output/utils/data-transformer-utils.ts @@ -0,0 +1,139 @@ +import jsyaml from 'js-yaml'; +import { TektonResourceLabel } from '../../../types'; +import { TaskRunKind } from '../../../types/taskrun'; +import { + ACSCheckResults, + ACSImageScanResult, + ComponentEnterpriseContractResult, + ENTERPRISE_CONTRACT_POLICY_STATUS, + EnterpriseContractPolicy, + EnterpriseContractResult, + EnterpriseContractRule, + OutputTaskRunGroup, + TaskRunResultsAnnotations, + TaskRunResultsFormatValue, + TaskRunResultsKeyValue, + TaskRunResultsTypeValue, +} from '../types'; + +const checkTypeAnnotation = (tr: TaskRunKind | undefined, type: TaskRunResultsTypeValue): boolean => + tr?.metadata?.annotations?.[TaskRunResultsAnnotations.TYPE] === type; + +export const isSbomTaskRun = (tr: TaskRunKind | undefined): boolean => + tr?.metadata?.annotations?.[TaskRunResultsAnnotations.KEY] === TaskRunResultsKeyValue.SBOM; + +export const isECTaskRun = (tr: TaskRunKind | undefined): boolean => + checkTypeAnnotation(tr, TaskRunResultsTypeValue.EC); + +export const isACSImageScanTaskRun = (tr: TaskRunKind | undefined): boolean => + checkTypeAnnotation(tr, TaskRunResultsTypeValue.ROXCTL_IMAGE_SCAN); + +export const isACSImageCheckTaskRun = (tr: TaskRunKind | undefined): boolean => + checkTypeAnnotation(tr, TaskRunResultsTypeValue.ROXCTL_IMAGE_CHECK); + +export const isACSDeploymentCheckTaskRun = (tr: TaskRunKind | undefined): boolean => + checkTypeAnnotation(tr, TaskRunResultsTypeValue.ROXCTL_DEPLOYMENT_CHECK); + +export const getTaskrunsOutputGroup = ( + pipelineRunName: string | undefined, + taskruns: TaskRunKind[], +): OutputTaskRunGroup => { + const getPLRTaskRunByType = ( + check: (tr: TaskRunKind | undefined) => boolean, + ): TaskRunKind | undefined => + taskruns?.find( + (tr: TaskRunKind) => + tr?.metadata?.labels?.[TektonResourceLabel.pipelinerun] === pipelineRunName && check(tr), + ); + + return { + sbomTaskRun: getPLRTaskRunByType(isSbomTaskRun), + ecTaskRun: getPLRTaskRunByType(isECTaskRun), + acsImageScanTaskRun: getPLRTaskRunByType(isACSImageScanTaskRun), + acsImageCheckTaskRun: getPLRTaskRunByType(isACSImageCheckTaskRun), + acsDeploymentCheckTaskRun: getPLRTaskRunByType(isACSDeploymentCheckTaskRun), + }; +}; + +export const hasExternalLink = (sbomTaskRun: TaskRunKind | undefined): boolean => + sbomTaskRun?.metadata?.annotations?.[TaskRunResultsAnnotations.TYPE] === + TaskRunResultsTypeValue.EXTERNAL_LINK; + +export const getSbomLink = (sbomTaskRun: TaskRunKind | undefined): string | undefined => + (sbomTaskRun?.status?.results || sbomTaskRun?.status?.taskResults)?.find( + (r: any) => r.name === TaskRunResultsKeyValue.SBOM, + )?.value; + +type ProcessedData = EnterpriseContractResult | ACSCheckResults | ACSImageScanResult | string; +export const formatData = (format: string, data: string) => { + const processData = (d: string): ProcessedData => { + const data = jsyaml.load(d) as ProcessedData; + return typeof data === 'object' && data !== null ? data : ''; + }; + switch (format) { + case TaskRunResultsFormatValue.JSON: + case TaskRunResultsFormatValue.YAML: + case TaskRunResultsFormatValue.TEXT: + return processData(data); + default: + return ''; + } +}; + +export const mapEnterpriseContractResultData = ( + ecResult: EnterpriseContractResult, +): EnterpriseContractPolicy[] => { + const components = ecResult + ? ecResult.components?.filter((comp: ComponentEnterpriseContractResult) => { + return !( + comp.violations && + comp.violations?.length === 1 && + !comp.violations[0].metadata && + comp.violations[0].msg.includes('404 Not Found') + ); + }) ?? [] + : []; + + return components?.reduce( + (acc: EnterpriseContractPolicy[], compResult: ComponentEnterpriseContractResult) => { + compResult?.violations?.forEach((v: EnterpriseContractRule) => { + const rule: EnterpriseContractPolicy = { + title: v.metadata?.title ?? '', + description: v.metadata?.description ?? '', + status: ENTERPRISE_CONTRACT_POLICY_STATUS.failed, + timestamp: v.metadata?.effective_on, + component: compResult.name, + msg: v.msg, + collection: v.metadata?.collections, + solution: v.metadata?.solution, + }; + acc.push(rule); + }); + compResult?.warnings?.forEach((w: EnterpriseContractRule) => { + const rule: EnterpriseContractPolicy = { + title: w.metadata?.title ?? '', + description: w.metadata?.description ?? '', + status: ENTERPRISE_CONTRACT_POLICY_STATUS.warnings, + timestamp: w.metadata?.effective_on, + component: compResult.name, + msg: w.msg, + collection: w.metadata?.collections, + }; + acc.push(rule); + }); + compResult?.successes?.forEach((s: EnterpriseContractRule) => { + const rule: EnterpriseContractPolicy = { + title: s.metadata?.title ?? '', + description: s.metadata?.description ?? '', + status: ENTERPRISE_CONTRACT_POLICY_STATUS.successes, + component: compResult.name, + collection: s.metadata?.collections, + }; + acc.push(rule); + }); + + return acc; + }, + [], + ); +}; diff --git a/packages/pipelines/src/components/index.ts b/packages/pipelines/src/components/index.ts index 59d45a8..f306a24 100644 --- a/packages/pipelines/src/components/index.ts +++ b/packages/pipelines/src/components/index.ts @@ -1,2 +1,3 @@ export { Output, EnterpriseContract, AdvancedClusterSecurity, ResultsList } from './Output'; export * from './Output/types'; +export * from './Output/hooks/usePipelineRunOutput'; diff --git a/packages/pipelines/src/index.ts b/packages/pipelines/src/index.ts index 07635cb..974d976 100644 --- a/packages/pipelines/src/index.ts +++ b/packages/pipelines/src/index.ts @@ -1 +1,3 @@ export * from './components'; +export * from './types'; +export * from './utils'; diff --git a/packages/pipelines/src/types/coreTekton.ts b/packages/pipelines/src/types/coreTekton.ts new file mode 100644 index 0000000..128dbc0 --- /dev/null +++ b/packages/pipelines/src/types/coreTekton.ts @@ -0,0 +1,187 @@ +import { K8sResourceCommon } from './k8s'; + +export type ResourceTarget = 'inputs' | 'outputs'; + +export type TektonParam = { + default?: string | string[]; + description?: string; + name: string; + type?: 'string' | 'array'; +}; + +type TektonTaskSteps = { + name: string; + args?: string[]; + command?: string[]; + image?: string; + computeResources?: {}[] | {}; + env?: { name: string; value: string }[]; + script?: string | string[]; +}; + +type TektonTaskStepsV1Beta1 = { + name: string; + args?: string[]; + command?: string[]; + image?: string; + resources?: {}[] | {}; + env?: { name: string; value: string }[]; + script?: string | string[]; +}; + +export type TaskResult = { + name: string; + type?: string; + value?: string; + description?: string; +}; + +export type TektonTaskSpec = { + metadata?: {}; + description?: string; + steps: TektonTaskSteps[]; + params?: TektonParam[]; + results?: TaskResult[]; + volumes?: {}; + workspaces?: TektonWorkspace[]; +}; + +type TektonResourceGroup = { + inputs?: ResourceType[]; + outputs?: ResourceType[]; +}; + +/** + * @deprecated + */ +export type TektonTaskSpecV1Beta1 = { + metadata?: {}; + description?: string; + steps: TektonTaskStepsV1Beta1[]; + params?: TektonParam[]; + resources?: TektonResourceGroup; + results?: TaskResult[]; + volumes?: {}; + workspaces?: TektonWorkspace[]; +}; + +/** + * @deprecated - upstream Workspaces are replacing Resources + */ +export type TektonResource = { + name: string; + optional?: boolean; + type: string; // TODO: limit to known strings +}; + +export type TektonWorkspace = { + name: string; + description?: string; + mountPath?: string; + readOnly?: boolean; + optional?: boolean; +}; + +export type TektonResultsRun = { + name: string; + value: string; + type?: string; +}; + +export interface Addon { + enablePipelinesAsCode: boolean; + params: Param[]; +} + +export interface Param { + name: string; + value: string; +} + +export interface Dashboard { + readonly: boolean; +} + +export enum MetricsLevel { + METRICS_PIPELINERUN_DURATION_TYPE = 'metrics.pipelinerun.duration-type', + METRICS_PIPELINERUN_LEVEL = 'metrics.pipelinerun.level', + METRICS_TASKRUN_DURATION_TYPE = 'metrics.taskrun.duration-type', + METRICS_TASKRUN_LEVEL = 'metrics.taskrun.level', +} + +export enum LevelTypes { + PIPELINE = 'pipeline', + PIPELINERUN = 'pipelinerun', + TASK = 'task', + TASKRUN = 'taskrun', +} + +export enum DurationTypes { + HISTOGRAM = 'histogram', + LASTVALUE = 'lastvalue', + NAMESPACE = 'namespace', +} + +export interface Pipeline { + 'default-service-account': string; + 'disable-affinity-assistant': boolean; + 'disable-creds-init': boolean; + 'enable-api-fields': string; + 'enable-custom-tasks': boolean; + 'enable-tekton-oci-bundles': boolean; + [MetricsLevel.METRICS_PIPELINERUN_DURATION_TYPE]: DurationTypes; + [MetricsLevel.METRICS_PIPELINERUN_LEVEL]: LevelTypes; + [MetricsLevel.METRICS_TASKRUN_DURATION_TYPE]: DurationTypes; + [MetricsLevel.METRICS_TASKRUN_LEVEL]: LevelTypes; + params: Param[]; + 'require-git-ssh-secret-known-hosts': boolean; + 'running-in-environment-with-injected-sidecars': boolean; + 'scope-when-expressions-to-task': boolean; +} + +export interface Pruner { + keep: number; + resources: string[]; + schedule: string; +} + +export interface Trigger { + 'default-service-account': string; + 'enable-api-fields': string; +} + +export interface Spec { + addon: Addon; + config: {}; + dashboard: Dashboard; + hub: {}; + params: Param[]; + pipeline: Pipeline; + profile: string; + pruner: Pruner; + targetNamespace: string; + trigger: Trigger; +} + +export interface Status { + conditions: TektonConfigCondition[]; +} + +export interface TektonConfigCondition { + lastTransitionTime: string; + status: string; + type: string; +} + +export type TektonConfig = K8sResourceCommon & { + spec: Spec; + status: Status; +}; + +export enum TektonResourceLabel { + pipeline = 'tekton.dev/pipeline', + pipelinerun = 'tekton.dev/pipelineRun', + taskrun = 'tekton.dev/taskRun', + task = 'tekton.dev/task', + pipelineTask = 'tekton.dev/pipelineTask', +} diff --git a/packages/pipelines/src/types/index.ts b/packages/pipelines/src/types/index.ts new file mode 100644 index 0000000..334fddc --- /dev/null +++ b/packages/pipelines/src/types/index.ts @@ -0,0 +1,5 @@ +export * from './coreTekton'; +export * from './pipeline'; +export * from './pipelinerun'; +export * from './task'; +export * from './taskrun'; diff --git a/packages/pipelines/src/types/k8s.ts b/packages/pipelines/src/types/k8s.ts new file mode 100644 index 0000000..8d0425b --- /dev/null +++ b/packages/pipelines/src/types/k8s.ts @@ -0,0 +1,98 @@ +export type K8sResourceIdentifier = { + apiGroup?: string; + apiVersion: string; + kind: string; +}; + +/** + * K8s status object used when Kubernetes cannot handle a request. + */ + +export type K8sStatus = K8sResourceIdentifier & { + code: number; + message: string; + reason: string; + status: 'Success' | 'Failure'; +}; + +export type OwnerReference = { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +}; + +export enum Operator { + Exists = 'Exists', + DoesNotExist = 'DoesNotExist', + In = 'In', + NotIn = 'NotIn', + Equals = 'Equals', + NotEqual = 'NotEqual', + GreaterThan = 'GreaterThan', + LessThan = 'LessThan', + NotEquals = 'NotEquals', +} + +export type MatchLabels = { + [key: string]: string; +}; + +export type Selector = Partial<{ + matchLabels: MatchLabels; + matchExpressions: MatchExpression[]; + [key: string]: unknown; +}>; + +export type MatchExpression = { + key: string; + operator: Operator | string; + values?: string[]; + value?: string; +}; + +export type ObjectMetadata = { + annotations?: { [key: string]: string }; + creationTimestamp?: string; + deletionGracePeriodSeconds?: number; + deletionTimestamp?: string; + finalizers?: string[]; + generateName?: string; + generation?: number; + labels?: { [key: string]: string }; + managedFields?: any[]; + name?: string; + namespace?: string; + ownerReferences?: object[]; + resourceVersion?: string; + uid?: string; +}; + +export type K8sResourceCommon = K8sResourceIdentifier & + Partial<{ + metadata: Partial<{ + annotations: Record; + clusterName: string; + creationTimestamp: string; + deletionGracePeriodSeconds: number; + deletionTimestamp: string; + finalizers: string[]; + generateName: string; + generation: number; + labels: Record; + managedFields: unknown[]; + name: string; + namespace: string; + ownerReferences: OwnerReference[]; + resourceVersion: string; + uid: string; + }>; + spec: { + selector?: Selector | MatchLabels; + [key: string]: unknown; + }; + status: { [key: string]: unknown }; + data: { [key: string]: unknown }; + }>; diff --git a/packages/pipelines/src/types/pipeline.ts b/packages/pipelines/src/types/pipeline.ts new file mode 100644 index 0000000..320d69e --- /dev/null +++ b/packages/pipelines/src/types/pipeline.ts @@ -0,0 +1,66 @@ +import { TektonParam, TektonTaskSpec, TektonWorkspace } from './coreTekton'; +import { K8sResourceCommon } from './k8s'; +import { TaskRunStatus } from './taskrun'; + +export type PipelineTaskRef = { + resolver?: string; + kind?: string; + name?: string; + params?: { + name: string; + value: string; + }[]; +}; + +export type PipelineTaskWorkspace = { + name: string; + workspace: string; + optional?: boolean; +}; + +export type PipelineTaskResource = { + name: string; + resource?: string; + from?: string[]; +}; + +export type PipelineTaskParam = { + name: string; + value: any; +}; + +export type WhenExpression = { + input: string; + operator: string; + values: string[]; +}; + +export type PipelineResult = { + name: string; + value: string; + description?: string; +}; + +export type PipelineTask = { + name: string; + params?: PipelineTaskParam[]; + runAfter?: string[]; + taskRef?: PipelineTaskRef; + taskSpec?: TektonTaskSpec; + when?: WhenExpression[]; + workspaces?: PipelineTaskWorkspace[]; + status?: TaskRunStatus; +}; + +export type PipelineSpec = { + params?: TektonParam[]; + serviceAccountName?: string; + tasks: PipelineTask[]; + workspaces?: TektonWorkspace[]; + finally?: PipelineTask[]; + results?: PipelineResult[]; +}; + +export type PipelineKind = K8sResourceCommon & { + spec: PipelineSpec; +}; diff --git a/packages/pipelines/src/types/pipelinerun.ts b/packages/pipelines/src/types/pipelinerun.ts new file mode 100644 index 0000000..aeb375c --- /dev/null +++ b/packages/pipelines/src/types/pipelinerun.ts @@ -0,0 +1,158 @@ +import { TektonResultsRun } from './coreTekton'; +import { K8sResourceCommon, ObjectMetadata } from './k8s'; +import { PipelineKind, PipelineSpec, WhenExpression } from './pipeline'; +import { TaskRunStatus } from './taskrun'; + +export type PLRTaskRunStep = { + container: string; + imageID?: string; + name: string; + waiting?: { + reason: string; + }; + running?: { + startedAt: string; + }; + terminated?: { + containerID: string; + exitCode: number; + finishedAt: string; + reason: string; + startedAt: string; + message?: string; + }; +}; + +export type PLRTaskRunData = { + pipelineTaskName: string; + status: TaskRunStatus; +}; + +type PLRTaskRuns = { + [taskRunName: string]: PLRTaskRunData; +}; + +export type VolumeTypeSecret = { + secretName: string; + items?: { + key: string; + path: string; + }[]; +}; + +export type VolumeTypeConfigMaps = { + name: string; + items?: { + key: string; + path: string; + }[]; +}; + +export type VolumeTypePVC = { + claimName: string; +}; + +export type PersistentVolumeClaimType = { + persistentVolumeClaim: VolumeTypePVC; +}; + +export type VolumeClaimTemplateType = { + volumeClaimTemplate: VolumeTypeClaim; +}; +export type VolumeTypeClaim = { + metadata?: ObjectMetadata; + spec: { + accessModes: string[]; + resources: { + requests: { + storage: string; + }; + }; + storageClassName?: string; + volumeMode?: string; + }; +}; + +export type Condition = { + type: string; + status: string; + reason?: string; + message?: string; + binding?: string; + lastTransitionTime?: string; +}; + +export type PipelineRunEmbeddedResourceParam = { name: string; value: string }; +export type PipelineRunEmbeddedResource = { + name: string; + resourceSpec: { + params: PipelineRunEmbeddedResourceParam[]; + type: string; + }; +}; +export type PipelineRunReferenceResource = { + name: string; + resourceRef: { + name: string; + }; +}; + +export type PipelineRunResource = PipelineRunReferenceResource | PipelineRunEmbeddedResource; + +export type PipelineRunWorkspace = { + name: string; + [volumeType: string]: + | VolumeTypeSecret + | VolumeTypeConfigMaps + | VolumeTypePVC + | VolumeTypeClaim + | {}; +}; + +export type PipelineRunParam = { + name: string; + value: string | string[]; + input?: string; + output?: string; + resource?: object; +}; + +export type PipelineRunStatus = { + succeededCondition?: string; + creationTimestamp?: string; + conditions?: Condition[]; + startTime?: string; + completionTime?: string; + taskRuns?: PLRTaskRuns; + pipelineSpec: PipelineSpec; + skippedTasks?: { + name: string; + reason?: string; + whenExpressions?: WhenExpression[]; + }[]; + results?: TektonResultsRun[]; +}; + +export type PipelineRunKind = K8sResourceCommon & { + spec: { + pipelineRef?: { name: string; resolver?: string }; + pipelineSpec?: PipelineSpec; + params?: PipelineRunParam[]; + workspaces?: PipelineRunWorkspace[]; + taskRunTemplate?: { + serviceAccountName?: string; + }; + timeout?: { + pipeline: string; + tasks: string; + finally: string; + }; + // Only used in a single case - cancelling a pipeline; should not be copied between PLRs + status?: 'StoppedRunFinally' | 'CancelledRunFinally' | 'PipelineRunPending'; + }; + status?: PipelineRunStatus; +}; + +export type PipelineWithLatest = PipelineKind & { + latestRun?: PipelineRunKind; +}; diff --git a/packages/pipelines/src/types/task.ts b/packages/pipelines/src/types/task.ts new file mode 100644 index 0000000..bd71a6b --- /dev/null +++ b/packages/pipelines/src/types/task.ts @@ -0,0 +1,6 @@ +import { TektonTaskSpec } from './coreTekton'; +import { K8sResourceCommon } from './k8s'; + +export type TaskKind = K8sResourceCommon & { + spec: TektonTaskSpec; +}; diff --git a/packages/pipelines/src/types/taskrun.ts b/packages/pipelines/src/types/taskrun.ts new file mode 100644 index 0000000..3d1d9d7 --- /dev/null +++ b/packages/pipelines/src/types/taskrun.ts @@ -0,0 +1,44 @@ +import { TektonResultsRun, TektonTaskSpec } from './coreTekton'; +import { K8sResourceCommon } from './k8s'; +import { PipelineTaskParam, PipelineTaskRef } from './pipeline'; +import { + Condition, + PLRTaskRunStep, + VolumeTypeConfigMaps, + VolumeTypePVC, + VolumeTypeSecret, +} from './pipelinerun'; +import { TaskKind } from './task'; + +export type TaskRunWorkspace = { + name: string; + volumeClaimTemplate?: any; + persistentVolumeClaim?: VolumeTypePVC; + configMap?: VolumeTypeConfigMaps; + emptyDir?: {}; + secret?: VolumeTypeSecret; + subPath?: string; +}; + +export type TaskRunStatus = { + completionTime?: string; + conditions?: Condition[]; + podName?: string; + startTime?: string; + steps?: PLRTaskRunStep[]; + results?: TektonResultsRun[]; + taskResults?: TektonResultsRun[]; + taskSpec?: TaskKind['spec']; +}; + +export type TaskRunKind = K8sResourceCommon & { + spec: { + taskRef?: PipelineTaskRef; + taskSpec?: TektonTaskSpec; + serviceAccountName?: string; + params?: PipelineTaskParam[]; + timeout?: string; + workspaces?: TaskRunWorkspace[]; + }; + status?: TaskRunStatus; +}; diff --git a/packages/pipelines/src/utils/__tests__/pipelinerun-utils.test.ts b/packages/pipelines/src/utils/__tests__/pipelinerun-utils.test.ts new file mode 100644 index 0000000..d09c81e --- /dev/null +++ b/packages/pipelines/src/utils/__tests__/pipelinerun-utils.test.ts @@ -0,0 +1,62 @@ +import { testPipelineRuns, DataState } from '../../__fixtures__/pipelinerun'; +import { PipelineRunKind } from '../../types/pipelinerun'; +import { RunStatus, pipelineRunStatus } from '../pipelinerun-utils'; + +describe('pipelinerun-utils', () => { + test('should return Succeeded status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.SUCCEEDED])).toBe(RunStatus.Succeeded); + }); + test('should return Running status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.RUNNING])).toBe(RunStatus.Running); + }); + test('should return Pending status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.PENDING])).toBe(RunStatus.Pending); + }); + + test('should return Failed status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.STOPPING])).toBe(RunStatus.Failed); + }); + test('should return Failed status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.FAILED])).toBe(RunStatus.Failed); + }); + test('should return Cancelling status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.CANCELLING])).toBe(RunStatus.Cancelling); + }); + test('should return Cancelled status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.CANCELLED])).toBe(RunStatus.Cancelled); + }); + + test('should return Cancelled status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.STOPPED])).toBe(RunStatus.Cancelled); + }); + + test('should return Skipped status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.SKIPPED])).toBe(RunStatus.Skipped); + }); + + test('should return Pending status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.EXCEEDED_NODE_RESOURCES])).toBe( + RunStatus.Pending, + ); + }); + + test('should return Pending status', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.STATUS_WITHOUT_CONDITIONS])).toBe( + RunStatus.Pending, + ); + }); + + test('should return Running status for unknown reason', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.STATUS_WITH_UNKNOWN_REASON])).toBe( + RunStatus.Running, + ); + }); + test('should return Pending status for unknown reason', () => { + expect(pipelineRunStatus(testPipelineRuns[DataState.STATUS_WITH_EMPTY_CONDITIONS])).toBe( + RunStatus.Pending, + ); + }); + test('should return Pending status for unknown reason', () => { + expect(pipelineRunStatus({} as PipelineRunKind)).toBe(RunStatus.Pending); + }); +}); diff --git a/packages/pipelines/src/utils/data-utils.ts b/packages/pipelines/src/utils/data-utils.ts new file mode 100644 index 0000000..123179a --- /dev/null +++ b/packages/pipelines/src/utils/data-utils.ts @@ -0,0 +1,348 @@ +import { TektonResourceLabel, TektonResultsRun } from '../types/coreTekton'; +import { PipelineTask } from '../types/pipeline'; +import { PipelineRunKind, Condition } from '../types/pipelinerun'; +import { TaskRunKind } from '../types/taskrun'; +import { RunStatus, SucceedConditionReason } from './pipelinerun-utils'; + +const samplePipelineRun: PipelineRunKind = { + apiVersion: 'tekton.dev/v1', + kind: 'PipelineRun', + metadata: { + annotations: { + 'pipeline.openshift.io/started-by': 'kube-admin', + 'chains.tekton.dev/signed': 'false', + }, + labels: { + 'backstage.io/kubernetes-id': 'test-backstage', + 'tekton.dev/pipeline': 'pipeline-test', + 'app.kubernetes.io/instance': 'abs', + 'app.kubernetes.io/name': 'ghg', + 'operator.tekton.dev/operand-name': 'ytui', + 'pipeline.openshift.io/runtime-version': 'hjkhk', + 'pipeline.openshift.io/type': 'hhu', + 'pipeline.openshift.io/runtime': 'node', + }, + name: 'pipelinerun-with-scanner-task', + namespace: 'deb-test', + resourceVersion: '117337', + uid: '0a091bbf-3813-48d3-a6ce-fc43644a9b24', + creationTimestamp: '2023-04-11T12:31:56Z', + }, + spec: { + pipelineRef: { + name: 'pipeline-test', + }, + serviceAccountName: 'pipeline', + workspaces: [], + }, + status: { + completionTime: '2023-04-11T06:49:05Z', + conditions: [ + { + lastTransitionTime: '2023-03-30T07:05:13Z', + message: 'Tasks Completed: 3 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + pipelineSpec: { + tasks: [ + { + name: 'scan-task', + params: [], + taskRef: { + kind: 'ClusterTask', + name: 'scan-task', + }, + workspaces: [], + }, + ], + workspaces: [], + }, + results: [ + { + name: 'SCAN_OUTPUT', + value: + '{"vulnerabilities":{\n"critical": 13,\n"high": 29,\n"medium": 32,\n"low": 3,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + ], + startTime: '2023-04-11T05:49:05Z', + }, +}; + +export const sampleTaskRun: TaskRunKind = { + apiVersion: 'tekton.dev/v1', + kind: 'TaskRun', + metadata: { + name: 'ec-taskrun', + labels: { + 'tekton.dev/pipelineRun': 'pipelinerun-with-scanner-task', + 'tekton.dev/pipelineTask': 'ec-task', + }, + annotations: { + 'chains.tekton.dev/signed': 'true', + 'pipeline.openshift.io/preferredName': 'pipelineRun-ec-task', + 'pipeline.openshift.io/started-by': 'kube:admin', + 'pipeline.tekton.dev/release': 'a2f17f6', + 'task.results.format': 'application/json', + 'task.output.location': 'logs', + 'task.results.type': 'ec', + name: 'pipelineRun-ec-task-t237ev', + uid: '764d0a6c-a4f6-419c-a3c3-585c2a9eb67c', + }, + }, + spec: { + serviceAccountName: 'pipeline', + taskRef: { + kind: 'Task', + name: 'ec-task', + }, + timeout: '1h0m0s', + }, + status: { + completionTime: '2023-11-08T08:18:25Z', + conditions: [ + { + lastTransitionTime: '2023-11-08T08:18:25Z', + message: 'All Steps have completed executing', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + podName: 'pipelineRun-ec-task-t237ev-pod', + }, +}; + +const samplePod = { + metadata: { + name: 'pipeline-test-wbvtlk-tkn-pod', + namespace: 'karthik', + uid: 'bd868fde-1b37-4168-a780-f1772c5924e3', + resourceVersion: '379524', + labels: { + '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'), + }, +}; + +type DataStateConditions = + | RunStatus + | SucceedConditionReason + | 'STATUS_WITHOUT_CONDITIONS' + | 'STATUS_WITH_EMPTY_CONDITIONS'; + +const runConditions: { [key in DataStateConditions]?: Condition | {} } = { + [RunStatus['In Progress']]: { status: 'Unknown', type: 'Succeeded' }, + [RunStatus.Running]: { status: 'Unknown', reason: 'Running', type: 'Succeeded' }, + [RunStatus.Succeeded]: { + lastTransitionTime: '2023-03-30T07:05:13Z', + message: 'Tasks Completed: 3 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + [RunStatus.Failed]: { + lastTransitionTime: '2023-03-30T07:05:13Z', + message: 'Tasks Completed: 1 (Failed: 1, Cancelled 0), Skipped: 0', + reason: 'Failed', + status: 'False', + type: 'Succeeded', + }, + + [RunStatus.Pending]: { + status: 'Unknown', + reason: 'CreateContainerConfigError', + type: 'Succeeded', + }, + [RunStatus.Skipped]: { + type: 'Succeeded', + status: 'Unknown', + reason: 'ConditionCheckFailed', + }, + [RunStatus.Cancelling]: { + type: 'Succeeded', + status: 'Unknown', + reason: 'CancelledRunFinally', + }, + [RunStatus.Cancelled]: { + type: 'Succeeded', + status: 'Unknown', + reason: 'Cancelled', + }, + [RunStatus.PipelineNotStarted]: { + type: 'Succeeded', + status: 'Unknown', + reason: 'Succeeded', + }, + [RunStatus.Unknown]: { + status: 'Unknown', + reason: 'Unknown', + type: 'Succeeded', + }, + [SucceedConditionReason.PipelineRunStopping]: { + type: 'Succeeded', + status: 'Unknown', + reason: 'PipelineRunStopping', + }, + [SucceedConditionReason.PipelineRunStopped]: { + type: 'Succeeded', + status: 'Unknown', + reason: 'StoppedRunFinally', + }, + [SucceedConditionReason.ExceededNodeResources]: { + status: 'Unknown', + reason: 'ExceededNodeResources', + type: 'ExceededNodeResources', + }, + STATUS_WITHOUT_CONDITIONS: {}, + STATUS_WITH_EMPTY_CONDITIONS: {}, +}; + +const sampleTask: (name: string) => PipelineTask = (name: string) => ({ + name, + params: [], + taskRef: { + kind: 'ClusterTask', + name, + }, +}); + +type ResourceConfig = { + name: string; + status: DataStateConditions; + labels?: { [key: string]: string }; + annotations?: { [key: string]: string }; + results?: TektonResultsRun[]; +}; + +export type mockPipelineRunConfig = ResourceConfig & { + spec?: any; + tasks: ResourceConfig[]; + createTaskRuns?: boolean; + createPods?: boolean; +}; + +export const createPipelineRunData = ( + config: mockPipelineRunConfig, +): { pipelineRun: PipelineRunKind; taskRuns?: TaskRunKind[]; pods?: any[] } => { + const tasks: PipelineTask[] = []; + const taskRuns: TaskRunKind[] = []; + const pods: any[] = []; + + const { name, status, spec } = config; + + const _createPipelineRun = (plrname: string) => { + return { + ...samplePipelineRun, + metadata: { + ...samplePipelineRun.metadata, + name: plrname, + labels: { + ...samplePipelineRun?.metadata?.labels, + [TektonResourceLabel.pipelinerun]: plrname, + }, + }, + spec: { + ...samplePipelineRun.spec, + tasks: tasks.length > 0 ? tasks : [sampleTask('sample-task')], + ...spec, + }, + status: { + ...samplePipelineRun.status, + conditions: status === 'STATUS_WITH_EMPTY_CONDITIONS' ? [] : [runConditions[status]], + }, + } as PipelineRunKind; + }; + + const _createPod = (task: PipelineTask): void => { + const taskRunName = `${task.name}-run`; + const podName = `${name}-${task.name}-pod`; + + const taskRunPod = { + ...samplePod, + metadata: { + ...samplePod?.metadata, + name: podName, + labels: { + ...sampleTaskRun?.metadata?.labels, + [TektonResourceLabel.pipelinerun]: name, + [TektonResourceLabel.pipelineTask]: task?.name, + [TektonResourceLabel.task]: task?.name, + [TektonResourceLabel.taskrun]: taskRunName, + }, + }, + }; + pods.push(taskRunPod); + }; + + const _createTaskRun = (task: PipelineTask, taskConfig: ResourceConfig): void => { + const taskRunName = `${task.name}-run`; + const podName = `${name}-${task.name}-pod`; + + const { status, labels, annotations, results } = taskConfig; + + const taskRun = { + ...sampleTaskRun, + metadata: { + ...sampleTaskRun?.metadata, + name: taskRunName, + labels: { + ...sampleTaskRun?.metadata?.labels, + [TektonResourceLabel.pipelinerun]: name, + [TektonResourceLabel.pipelineTask]: task?.name, + ...(labels ? labels : {}), + }, + annotations: { + ...sampleTaskRun?.metadata?.labels, + ...(annotations ? annotations : {}), + }, + }, + spec: task, + status: { + ...sampleTaskRun.status, + conditions: [runConditions[status] as Condition], + ...(results ? { results } : { results: [] }), + podName, + }, + }; + taskRuns.push(taskRun); + }; + + config.tasks.forEach((t, i) => { + const task = sampleTask(t.name); + tasks.push(sampleTask(t.name)); + config.createTaskRuns && _createTaskRun(task, t); + config.createPods && _createPod(task); + }); + + return { + pipelineRun: _createPipelineRun(config.name), + taskRuns, + pods, + }; +}; diff --git a/packages/pipelines/src/utils/index.ts b/packages/pipelines/src/utils/index.ts index e69de29..8fa3c0e 100644 --- a/packages/pipelines/src/utils/index.ts +++ b/packages/pipelines/src/utils/index.ts @@ -0,0 +1 @@ +export { createPipelineRunData } from './data-utils'; diff --git a/packages/pipelines/src/utils/pipelinerun-utils.ts b/packages/pipelines/src/utils/pipelinerun-utils.ts new file mode 100644 index 0000000..f4efc49 --- /dev/null +++ b/packages/pipelines/src/utils/pipelinerun-utils.ts @@ -0,0 +1,91 @@ +import { Condition, PipelineRunKind } from '../types/pipelinerun'; + +export enum SucceedConditionReason { + PipelineRunStopped = 'StoppedRunFinally', + PipelineRunCancelled = 'CancelledRunFinally', + TaskRunCancelled = 'TaskRunCancelled', + Cancelled = 'Cancelled', + PipelineRunStopping = 'PipelineRunStopping', + PipelineRunPending = 'PipelineRunPending', + TaskRunStopping = 'TaskRunStopping', + CreateContainerConfigError = 'CreateContainerConfigError', + ExceededNodeResources = 'ExceededNodeResources', + ExceededResourceQuota = 'ExceededResourceQuota', + ConditionCheckFailed = 'ConditionCheckFailed', +} + +export enum RunStatus { + Succeeded = 'Succeeded', + Failed = 'Failed', + Running = 'Running', + 'In Progress' = 'In Progress', + FailedToStart = 'FailedToStart', + PipelineNotStarted = 'Starting', + WithoutStatusConditions = 'WithoutStatusConditions', + NeedsMerge = 'PR needs merge', + Skipped = 'Skipped', + Cancelled = 'Cancelled', + Cancelling = 'Cancelling', + Pending = 'Pending', + Idle = 'Idle', + TestWarning = 'Test Warnings', + TestFailed = 'Test Failures', + Unknown = 'Unknown', +} + +export const conditionsRunStatus = (conditions: Condition[], specStatus?: string): RunStatus => { + if (!conditions?.length) { + return RunStatus.Pending; + } + + const cancelledCondition = conditions.find((c) => c.reason === 'Cancelled'); + const succeedCondition = conditions.find((c) => c.type === 'Succeeded'); + + if (!succeedCondition || !succeedCondition.status) { + return RunStatus.Pending; + } + + const status = + succeedCondition.status === 'True' + ? RunStatus.Succeeded + : succeedCondition.status === 'False' + ? RunStatus.Failed + : RunStatus.Running; + + if ( + [ + `${SucceedConditionReason.PipelineRunStopped}`, + `${SucceedConditionReason.PipelineRunCancelled}`, + ].includes(specStatus ?? '') && + !cancelledCondition + ) { + return RunStatus.Cancelling; + } + + if (!succeedCondition.reason || succeedCondition.reason === status) { + return status; + } + + switch (succeedCondition.reason) { + case SucceedConditionReason.PipelineRunStopped: + case SucceedConditionReason.PipelineRunCancelled: + case SucceedConditionReason.TaskRunCancelled: + case SucceedConditionReason.Cancelled: + return RunStatus.Cancelled; + case SucceedConditionReason.PipelineRunStopping: + case SucceedConditionReason.TaskRunStopping: + return RunStatus.Failed; + case SucceedConditionReason.CreateContainerConfigError: + case SucceedConditionReason.ExceededNodeResources: + case SucceedConditionReason.ExceededResourceQuota: + case SucceedConditionReason.PipelineRunPending: + return RunStatus.Pending; + case SucceedConditionReason.ConditionCheckFailed: + return RunStatus.Skipped; + default: + return status; + } +}; + +export const pipelineRunStatus = (pipelineRun: PipelineRunKind): RunStatus => + conditionsRunStatus(pipelineRun?.status?.conditions ?? [], pipelineRun?.spec?.status); diff --git a/yarn.lock b/yarn.lock index 876bdf6..0ed1668 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3367,6 +3367,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/js-yaml@^4.0.5": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/jsdom@^20.0.0": version "20.0.1" resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz"