diff --git a/base/200-clusterrole-backend.yaml b/base/200-clusterrole-backend.yaml index b13e82e50..7baf52531 100644 --- a/base/200-clusterrole-backend.yaml +++ b/base/200-clusterrole-backend.yaml @@ -54,3 +54,7 @@ rules: - get - list - watch + # Dashboard needs to be able to query existing results. + - apiGroups: ["results.tekton.dev"] + resources: ["logs", "results", "records"] + verbs: ["get", "list"] diff --git a/base/300-deployment.yaml b/base/300-deployment.yaml index b0e13c681..e306a0b2c 100644 --- a/base/300-deployment.yaml +++ b/base/300-deployment.yaml @@ -80,3 +80,5 @@ spec: runAsNonRoot: true seccompProfile: type: RuntimeDefault + # empty volumeMounts is needed for resultsAPI cert mount + volumeMounts: [] diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 91018d8e9..2647efd76 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -23,6 +23,7 @@ import ( "github.com/tektoncd/dashboard/pkg/router" k8sclientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + certutil "k8s.io/client-go/util/cert" ) var ( @@ -38,6 +39,9 @@ var ( streamLogs = flag.Bool("stream-logs", true, "Enable log streaming instead of polling") externalLogs = flag.String("external-logs", "", "External logs provider URL") xFrameOptions = flag.String("x-frame-options", "DENY", "Value for the X-Frame-Options response header, set '' to omit it") + resultsApiAddr = flag.String("results-api-addr", "tekton-results-api-service.tekton-pipelines.svc.cluster.local:8080", "Address of Results API server") + enableResultsTLS = flag.Bool("enable-results-tls", false, "Enable TLS verification when connecting to Results API server") + resultsTLSCertPath = flag.String("results-tls-cert-path", "/etc/tls/tls.crt", "TLS certs to the Results API server.") ) func main() { @@ -63,6 +67,11 @@ func main() { } tenants := strings.FieldsFunc(*tenantNamespaces, splitByComma) + resultsCfg, err := resultsConfig(*enableResultsTLS, *resultsTLSCertPath) + if err != nil { + logging.Log.Errorf("Error building results config: %s", err.Error()) + } + options := endpoints.Options{ InstallNamespace: installNamespace, PipelinesNamespace: *pipelinesNamespace, @@ -80,9 +89,10 @@ func main() { Config: cfg, K8sClient: k8sClient, Options: options, + ResultsConfig: resultsCfg, } - server, err := router.Register(resource, cfg) + server, err := router.Register(resource, cfg, resultsCfg) if err != nil { logging.Log.Errorf("Error creating proxy: %s", err.Error()) @@ -99,3 +109,25 @@ func main() { logging.Log.Infof("Starting to serve on %s", l.Addr().String()) logging.Log.Fatal(server.ServeOnListener(l)) } + +func resultsConfig(enableResultsTLS bool, resultsTLSCertPath string) (*rest.Config, error) { + var cfg *rest.Config + var err error + + if cfg, err = rest.InClusterConfig(); err != nil { + logging.Log.Errorf("Error building kubeconfig: %s", err.Error()) + return nil, err + } + if enableResultsTLS { + if _, err := certutil.NewPool(resultsTLSCertPath); err != nil { + logging.Log.Errorf("Expected to load root CA config from %s, but got err: %v", resultsTLSCertPath, err) + } else { + cfg.TLSClientConfig.CAFile = resultsTLSCertPath + } + } else { + cfg.TLSClientConfig.Insecure = true + cfg.TLSClientConfig.CAFile = "" + } + cfg.Host = "https://" + *resultsApiAddr + return cfg, nil +} diff --git a/docs/dev/results.md b/docs/dev/results.md new file mode 100644 index 000000000..8a94bd28d --- /dev/null +++ b/docs/dev/results.md @@ -0,0 +1,16 @@ +# Tekton Dashboard - [Tekton Results](https://github.com/tektoncd/results) API support + +Tekton Results aims to help users logically group CI/CD workload history and separate out long term result storage away +from the Pipeline controller. For more info information, please +see [Tekton Results](https://github.com/tektoncd/results) + +Note: Dashboard for [Tekton Results](https://github.com/tektoncd/results) API support is still at early stage. + +## [Tekton Results](https://github.com/tektoncd/results) supporting set-up + +1. follow Tekton Results [installation instructions](https://github.com/tektoncd/results/blob/main/docs/install.md) +2. append `--enable-results` arguments to install, note that `--enable-results` override `--read-write`. + +```bash +./scripts/installer install --enable-results +``` diff --git a/overlays/installer/results/kustomization.yaml b/overlays/installer/results/kustomization.yaml new file mode 100644 index 000000000..5378669d0 --- /dev/null +++ b/overlays/installer/results/kustomization.yaml @@ -0,0 +1,27 @@ +# Copyright 2020-2023 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ../read-only +patches: +- path: ../../patches/results/deployment-tls-patch.yaml + target: + group: apps + kind: Deployment + name: tekton-dashboard + namespace: tekton-pipelines + version: v1 diff --git a/overlays/patches/results/deployment-tls-patch.yaml b/overlays/patches/results/deployment-tls-patch.yaml new file mode 100644 index 000000000..59981638e --- /dev/null +++ b/overlays/patches/results/deployment-tls-patch.yaml @@ -0,0 +1,35 @@ +# Copyright 2020-2021 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +- op: add + path: /spec/template/spec/volumes/- + value: + name: tls + secret: + secretName: tekton-results-tls +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: tls + mountPath: "/etc/tls" + readOnly: true +- op: add + path: /spec/template/spec/containers/0/args/- + value: + --enable-results-tls=1 +- op: add + path: /spec/template/spec/containers/0/args/- + value: + --results-tls-cert-path=/etc/tls/tls.crt diff --git a/packages/components/src/components/TaskRuns/TaskRuns.jsx b/packages/components/src/components/TaskRuns/TaskRuns.jsx index b4c355e8a..7ff8a7200 100644 --- a/packages/components/src/components/TaskRuns/TaskRuns.jsx +++ b/packages/components/src/components/TaskRuns/TaskRuns.jsx @@ -138,7 +138,8 @@ const TaskRuns = ({ const statusIcon = getTaskRunStatusIcon(taskRun); const taskRunURL = getTaskRunURL({ name: taskRunName, - namespace + namespace, + taskRun }); const taskRunsURL = diff --git a/packages/utils/src/utils/router.js b/packages/utils/src/utils/router.js index 6d1236d1c..02578f3f6 100644 --- a/packages/utils/src/utils/router.js +++ b/packages/utils/src/utils/router.js @@ -197,6 +197,17 @@ export const paths = { byNamespace() { return byNamespace({ path: '/triggertemplates' }); } + }, + taskRunsByResults: { + all() { + return '/results/taskruns'; + }, + byNamespace() { + return byNamespace({ path: '/results/taskruns' }); + }, + byUID() { + return byNamespace({ path: '/results/taskruns/:resultuid/:recorduid' }); + } } }; diff --git a/pkg/endpoints/types.go b/pkg/endpoints/types.go index 6a6d0bcf3..f1838819a 100644 --- a/pkg/endpoints/types.go +++ b/pkg/endpoints/types.go @@ -54,7 +54,8 @@ func (o Options) GetTriggersNamespace() string { // Resource is a wrapper around all necessary clients and config used for endpoints type Resource struct { - Config *rest.Config - K8sClient k8sclientset.Interface - Options Options + Config *rest.Config + K8sClient k8sclientset.Interface + Options Options + ResultsConfig *rest.Config } diff --git a/pkg/router/router.go b/pkg/router/router.go index 474ad3f30..369f5c58d 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -122,7 +122,7 @@ func makeUpgradeTransport(config *rest.Config, keepalive time.Duration) (proxy.U } // Register returns a HTTP handler with the Dashboard and Kubernetes APIs registered -func Register(r endpoints.Resource, cfg *rest.Config) (*Server, error) { +func Register(r endpoints.Resource, cfg *rest.Config, resultsCfg *rest.Config) (*Server, error) { logging.Log.Info("Adding Kube API") apiProxyPrefix := "/api/" apisProxyPrefix := "/apis/" @@ -134,6 +134,14 @@ func Register(r endpoints.Resource, cfg *rest.Config) (*Server, error) { mux.Handle(apiProxyPrefix, proxyHandler) mux.Handle(apisProxyPrefix, proxyHandler) + logging.Log.Info("Adding Results API") + resultsApiProxyPrefix := "/apis/results.tekton.dev/" + resultsProxyHandler, err := NewProxyHandler(resultsCfg, 30*time.Second) + if err != nil { + return nil, err + } + mux.Handle(resultsApiProxyPrefix, resultsProxyHandler) + logging.Log.Info("Adding Dashboard APIs") registerWeb(r, mux) registerPropertiesEndpoint(r, mux) @@ -158,14 +166,17 @@ func NewProxyHandler(cfg *rest.Config, keepalive time.Duration) (http.Handler, e responder := &responder{} transport, err := rest.TransportFor(cfg) if err != nil { - return nil, err - } - upgradeTransport, err := makeUpgradeTransport(cfg, keepalive) - if err != nil { + logging.Log.Error(err) return nil, err } proxy := proxy.NewUpgradeAwareHandler(target, transport, false, false, responder) - proxy.UpgradeTransport = upgradeTransport + if !cfg.TLSClientConfig.Insecure { + upgradeTransport, err := makeUpgradeTransport(cfg, keepalive) + if err != nil { + return nil, err + } + proxy.UpgradeTransport = upgradeTransport + } proxy.UseRequestLocation = true proxy.UseLocationHost = true diff --git a/scripts/installer b/scripts/installer index 693e2bf03..e16590c97 100755 --- a/scripts/installer +++ b/scripts/installer @@ -13,6 +13,7 @@ # dashboard flavour READONLY="true" +RESULTS_SUPPORT="false" # configuration default values DEBUG="false" @@ -105,7 +106,9 @@ debug() { compile() { local overlay="overlays/installer" - if [ "$READONLY" == "true" ]; then + if [ "$RESULTS_SUPPORT" == "true" ]; then + overlay="$overlay/results" + elif [ "$READONLY" == "true" ]; then overlay="$overlay/read-only" else overlay="$overlay/read-write" @@ -409,6 +412,7 @@ help () { echo -e "\t[--tenant-namespaces ]\tWill limit the visibility to the specified comma-separated namespaces only" echo -e "\t[--triggers-namespace ]\tOverride the namespace where Tekton Triggers is installed (defaults to Dashboard install namespace)" echo -e "\t[--version ]\t\t\tWill download manifests for specified version or build everything using kustomize/ko" + echo -e "\t[--enable-results]\t\t\tWill build manifests that enable Tekton Results support" } # cleanup temporary files @@ -536,6 +540,9 @@ while [[ $# -gt 0 ]]; do '--preserve-import-paths') KO_RESOLVE_OPTIONS="$KO_RESOLVE_OPTIONS --preserve-import-paths" ;; + '--enable-results') + RESULTS_SUPPORT="true" + ;; *) echo "ERROR: Unknown option $1" help diff --git a/src/api/index.js b/src/api/index.js index 8e2d6e3be..23cac2637 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -21,6 +21,7 @@ import { get, getAPIRoot, post } from './comms'; import { apiRoot, getKubeAPI, + getLogAPI, getTektonPipelinesAPIVersion, isLogTimestampsEnabled, tektonAPIGroup, @@ -178,6 +179,11 @@ export function getPodLog({ container, name, namespace, stream }) { return get(uri, { Accept: 'text/plain,*/*' }, { stream }); } +export function getLogByResultsAPI({ namespace, resultUID, recordUID }) { + const uri = getLogAPI({ namespace, resultUID, recordUID }); + return get(uri, { Accept: 'text/plain,*/*' }, { stream: false }); +} + export function importResources({ importerNamespace, labels, diff --git a/src/api/taskRunsByResultsAPI.js b/src/api/taskRunsByResultsAPI.js new file mode 100644 index 000000000..b5dbc196b --- /dev/null +++ b/src/api/taskRunsByResultsAPI.js @@ -0,0 +1,100 @@ +/* +Copyright 2024 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useQuery } from '@tanstack/react-query'; +import { getRecordAPI, getRecordsAPI } from './utils'; +import { checkStatus } from './comms'; + +// useTaskRunsByResultsAPI list all TaskRuns by ResultsAPI +// Return a list of Records +export function useTaskRunsByResultsAPI(namespace, queryConfig) { + const query = useQuery({ + queryKey: ['results', 'taskruns', namespace], + queryFn: async () => { + const uri = getRecordsAPI({ + group: 'results.tekton.dev', + version: 'v1alpha2', + namespace, + result: '-', + filters: 'data_type==TASK_RUN' + }); + const resp = await fetch(uri, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }).then(response => checkStatus(response, false)); + + if (resp.records) { + resp.records = resp.records.map(record => { + return { + ...record, + data: { + type: record.data.type, + value: JSON.parse(atob(record.data.value)) + } + }; + }); + } + return resp; + }, + staleTime: 0, + ...queryConfig + }); + return { + ...query, + data: query.data?.records || [] + }; +} + +// useTaskRunByResultsAPI get a single TaskRun by ResultsAPI +// Return a single Record +export function useTaskRunByResultsAPI( + namespace, + resultUID, + recordUID, + queryConfig +) { + const query = useQuery({ + queryKey: ['results', 'taskrun', resultUID, recordUID, recordUID], + queryFn: async () => { + const uri = getRecordAPI({ + group: 'results.tekton.dev', + version: 'v1alpha2', + namespace, + resultUID, + recordUID + }); + const resp = await fetch(uri, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }).then(response => checkStatus(response, false)); + if (resp.data) { + if (!resp.data.type.includes('TaskRun')) { + throw new Error( + `Found a record of type other than taskrun: ${resp.data.type}` + ); + } + resp.data = { + type: resp.data.type, + value: JSON.parse(atob(resp.data.value)) + }; + } + return resp; + }, + ...queryConfig + }); + return query; +} diff --git a/src/api/taskRunsByResultsAPI.test.js b/src/api/taskRunsByResultsAPI.test.js new file mode 100644 index 000000000..ab98a3a72 --- /dev/null +++ b/src/api/taskRunsByResultsAPI.test.js @@ -0,0 +1,18 @@ +/* +Copyright 2024 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import * as API from './taskRunsByResultsAPI'; + +// TODO(xinnjie): add tests. Un-runnable test to block PR merge. +it('useTaskRunsByResults', () => { + expect(API.useTaskRunsByResultsAPI()).toEqual(null); +}); diff --git a/src/api/utils.js b/src/api/utils.js index c22e9572e..0f4ea6fc7 100644 --- a/src/api/utils.js +++ b/src/api/utils.js @@ -21,6 +21,8 @@ export const apiRoot = getAPIRoot(); export const tektonAPIGroup = 'tekton.dev'; export const triggersAPIGroup = 'triggers.tekton.dev'; export const dashboardAPIGroup = 'dashboard.tekton.dev'; +export const resultsAPIGroup = 'results.tekton.dev'; +export const ALL_RESULTS = '*'; export function getQueryParams({ filters, @@ -83,6 +85,55 @@ export function getKubeAPI({ ].join(''); } +// getRecordsAPI return URL for getting a list of records through the ResultsAPI +export function getRecordsAPI({ + group = resultsAPIGroup, + version = 'v1alpha2', + namespace = ALL_NAMESPACES, + result = ALL_RESULTS, + filters +}) { + return [ + `${apiRoot}/apis/${group}/${version}`, + namespace === ALL_NAMESPACES ? `/parents/-` : `/parents/${namespace}`, + result === ALL_RESULTS ? `/results/-` : `/results/${result}`, + `/records`, + filters ? `?filter=${encodeURIComponent(filters)}` : '' + ].join(''); +} + +// getRecordAPI return URL for getting a record through the ResultsAPI +export function getRecordAPI({ + group = resultsAPIGroup, + version = 'v1alpha2', + namespace, + resultUID, + recordUID +}) { + return [ + `${apiRoot}/apis/${group}/${version}`, + `/parents/${namespace}`, + `/results/${resultUID}`, + `/records/${recordUID}` + ].join(''); +} + +// getLogAPI return URL for getting a log through the ResultsAPI +export function getLogAPI({ + group = resultsAPIGroup, + version = 'v1alpha2', + namespace, + resultUID, + recordUID +}) { + return [ + `${apiRoot}/apis/${group}/${version}`, + `/parents/${namespace}`, + `/results/${resultUID}`, + `/logs/${recordUID}` + ].join(''); +} + export async function defaultQueryFn({ queryKey, signal }) { const [group, version, kind, params] = queryKey; const url = getKubeAPI({ group, kind, params, version }); @@ -194,9 +245,11 @@ export function useWebSocket({ function handleClose() { setWebSocketConnected(false); } + function handleOpen() { setWebSocketConnected(true); } + function handleMessage(event) { if (event.type !== 'message') { return; diff --git a/src/containers/SideNav/SideNav.jsx b/src/containers/SideNav/SideNav.jsx index 2c5890d58..25ebe31db 100644 --- a/src/containers/SideNav/SideNav.jsx +++ b/src/containers/SideNav/SideNav.jsx @@ -127,6 +127,12 @@ function SideNav({ expanded, showKubernetesResources = false }) { TaskRuns + {/* TODO(xinnjie) appear by config */} + + TaskRuns by Results + diff --git a/src/containers/TaskRunByResults/TaskRunByResults.jsx b/src/containers/TaskRunByResults/TaskRunByResults.jsx new file mode 100644 index 000000000..c486482ba --- /dev/null +++ b/src/containers/TaskRunByResults/TaskRunByResults.jsx @@ -0,0 +1,246 @@ +/* +Copyright 2024 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Fragment, useRef, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { SkeletonText } from '@carbon/react'; +import { + Log, + Portal, + RunHeader, + StepDetails, + TaskRunDetails, + TaskTree +} from '@tektoncd/dashboard-components'; +import { + getStatus, + getStepDefinition, + getStepStatus, + labels as labelConstants, + queryParams as queryParamConstants, + urls, + useTitleSync +} from '@tektoncd/dashboard-utils'; +import { getLogsToolbar, getViewChangeHandler } from '../../utils'; +import { useTaskRunByResultsAPI } from '../../api/taskRunsByResultsAPI'; +import { getLogByResultsAPI, useSelectedNamespace } from '../../api'; +import NotFound from '../NotFound'; + +const { STEP, RETRY, TASK_RUN_DETAILS, VIEW } = queryParamConstants; + +export function TaskRunContainerByResults() { + const location = useLocation(); + const navigate = useNavigate(); + const params = useParams(); + + const { + namespace: namespaceParam, + resultuid: resultUID, + recorduid: recordUID + } = params; + const queryParams = new URLSearchParams(location.search); + let currentRetry = queryParams.get(RETRY); + if (!currentRetry || !/^[0-9]+$/.test(currentRetry)) { + // if retry param is specified it should contain a positive integer (or 0) only + // otherwise we'll default to the latest attempt + currentRetry = ''; + } + const selectedStepId = queryParams.get(STEP); + const view = queryParams.get(VIEW); + const showTaskRunDetails = queryParams.get(TASK_RUN_DETAILS); + + const { selectedNamespace } = useSelectedNamespace(); + const namespace = namespaceParam || selectedNamespace; + + const maximizedLogsContainer = useRef(); + const [isLogsMaximized, setIsLogsMaximized] = useState(false); + const isUsingExternalLogs = false; + + const { + data: record, + error, + isLoading + } = useTaskRunByResultsAPI(namespace, resultUID, recordUID, { + staleTime: 1000 // 1 second + }); + + const taskRun = record?.data?.value; + + useTitleSync({ + page: 'TaskRun by Results', + resourceName: taskRun?.metadata?.name + }); + + function handleRetryChange(retry) { + if (Number.isInteger(retry)) { + queryParams.set(RETRY, retry); + } else { + queryParams.delete(RETRY); + } + const browserURL = location.pathname.concat(`?${queryParams.toString()}`); + navigate(browserURL); + } + + function handleTaskSelected({ selectedStepId: newSelectedStepId }) { + if (newSelectedStepId) { + queryParams.set(STEP, newSelectedStepId); + queryParams.delete(TASK_RUN_DETAILS); + } else { + queryParams.delete(STEP); + queryParams.set(TASK_RUN_DETAILS, true); + } + + if (newSelectedStepId !== selectedStepId) { + queryParams.delete(VIEW); + } + + const browserURL = location.pathname.concat(`?${queryParams.toString()}`); + if (showTaskRunDetails || selectedStepId) { + navigate(browserURL); + } else { + // auto-selecting step on first load + navigate(browserURL, { replace: true }); + } + } + + function toggleLogsMaximized() { + setIsLogsMaximized(state => !state); + } + + function getLogContainer({ stepName, stepStatus, taskRun: run }) { + if (!selectedStepId || !stepStatus) { + return null; + } + + // TODO(xinnjie): supporting Step level log, log provided by ResultsAPI is currently TaskRun level instead of Step level + const log = getLogByResultsAPI({ + namespace, + resultUID, + recordUID + }); + + const LogsRoot = isLogsMaximized ? Portal : Fragment; + + return ( + + log} + key={`${stepName}:${currentRetry}`} + stepStatus={stepStatus} + isLogsMaximized={isLogsMaximized} + enableLogAutoScroll + enableLogScrollButtons + /> + + ); + } + + if (isLoading) { + return ; + } + + if (error || !record || !taskRun) { + return ( + + ); + } + + const { + reason: taskRunStatusReason, + message: taskRunStatusMessage, + status: succeeded + } = getStatus(taskRun); + + const definition = getStepDefinition({ + selectedStepId, + task: null, + taskRun + }); + + const stepStatus = getStepStatus({ + selectedStepId, + taskRun + }); + + const onViewChange = getViewChangeHandler({ location, navigate }); + + const logContainer = getLogContainer({ + stepName: selectedStepId, + stepStatus, + taskRun + }); + + return ( + <> + +
+ + {(selectedStepId && ( + + )) || ( + + )} +
+ + ); +} + +export default TaskRunContainerByResults; diff --git a/src/containers/TaskRunByResults/index.js b/src/containers/TaskRunByResults/index.js new file mode 100644 index 000000000..36279d665 --- /dev/null +++ b/src/containers/TaskRunByResults/index.js @@ -0,0 +1,13 @@ +/* +Copyright 2024 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +export { default } from './TaskRunByResults'; diff --git a/src/containers/TaskRunsByResults/TaskRunsByResults.jsx b/src/containers/TaskRunsByResults/TaskRunsByResults.jsx new file mode 100644 index 000000000..16455754a --- /dev/null +++ b/src/containers/TaskRunsByResults/TaskRunsByResults.jsx @@ -0,0 +1,84 @@ +/* +Copyright 2024 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { urls } from '@tektoncd/dashboard-utils'; +import { useParams } from 'react-router-dom'; +import { TaskRuns as TaskRunsList } from '@tektoncd/dashboard-components'; +import NotFound from '../NotFound'; +import { useSelectedNamespace } from '../../api'; +import { useTaskRunsByResultsAPI } from '../../api/taskRunsByResultsAPI'; +import ListPageLayout from '../ListPageLayout'; + +const recordAnnotationKey = 'results.tekton.dev/record'; +function TaskRunsByResults() { + function getTaskRunURL({ namespace, taskRun }) { + // TODO(xinnjie): best user experience will be like if taskRun is still exist in kubernetes, provide a link to the taskRun page with live info and logs. + // if not, provide a link to the taskRun page by ResultsAPI + const recordAnnotation = taskRun.metadata.annotations[recordAnnotationKey]; + // record annotation format: default/results/8b19a00c-d702-4903-a9eb-d41d37250240/records/8b19a00c-d702-4903-a9eb-d41d37250240 + const [, , resultuid, , recorduid] = recordAnnotation.split('/'); + return urls.taskRunsByResults.byUID({ + namespace, + resultuid, + recorduid + }); + } + const params = useParams(); + const { namespace: namespaceParam } = params; + const { selectedNamespace } = useSelectedNamespace(); + const namespace = namespaceParam || selectedNamespace; + + const { + data: recordsForTaskRuns = [], + error, + isLoading + } = useTaskRunsByResultsAPI(namespace, { + staleTime: 1000 // 1 second + }); + + if (isLoading) { + return
Loading...
; + } + if (error) { + return ( + + ); + } + const taskRuns = recordsForTaskRuns.map(record => record.data.value); + + return ( + + {({ resources }) => ( + + )} + + ); +} + +export default TaskRunsByResults; diff --git a/src/containers/TaskRunsByResults/index.js b/src/containers/TaskRunsByResults/index.js new file mode 100644 index 000000000..27545bfa2 --- /dev/null +++ b/src/containers/TaskRunsByResults/index.js @@ -0,0 +1,15 @@ +/* +Copyright 2019-2021 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +export { default } from './TaskRunsByResults'; diff --git a/src/containers/index.js b/src/containers/index.js index 5dacdc021..163f8663f 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -45,6 +45,8 @@ export { default as Tasks } from './Tasks'; export { default as TasksDropdown } from './TasksDropdown'; export { default as TaskRun } from './TaskRun'; export { default as TaskRuns } from './TaskRuns'; +export { default as TaskRunByResults } from './TaskRunByResults'; +export { default as TaskRunsByResults } from './TaskRunsByResults'; export { default as TriggerBinding } from './TriggerBinding'; export { default as Trigger } from './Trigger'; export { default as TriggerTemplate } from './TriggerTemplate'; diff --git a/src/routes/pipelines.jsx b/src/routes/pipelines.jsx index bd8e4e643..3d9eadded 100644 --- a/src/routes/pipelines.jsx +++ b/src/routes/pipelines.jsx @@ -27,7 +27,9 @@ import { ReadWriteRoute, ResourceList, TaskRun, + TaskRunByResults, TaskRuns, + TaskRunsByResults, Tasks } from '../containers'; import { getTektonPipelinesAPIVersion, tektonAPIGroup } from '../api/utils'; @@ -240,5 +242,26 @@ export default [ ) + }, + { + path: paths.taskRunsByResults.all(), + element: + }, + { + path: paths.taskRunsByResults.byNamespace(), + element: , + handle: { + isNamespaced: true, + path: paths.taskRunsByResults.byNamespace() + } + }, + { + path: paths.taskRunsByResults.byUID(), + element: , + handle: { + isNamespaced: true, + isResourceDetails: true, + path: paths.taskRunsByResults.byUID() + } } ];