@kui-shell/plugin-tekton
Version:
Visualizations for Tekton Pipelines
417 lines (356 loc) • 16.2 kB
text/typescript
/*
* Copyright 2019 IBM Corporation
*
* 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 Debug from 'debug'
import * as prettyPrintDuration from 'pretty-ms'
import { KubeResource } from '@kui-shell/plugin-kubeui'
import { Badge, Mode, Tab, empty, prettyPrintTime, i18n } from '@kui-shell/core'
import { ActivationLikeFull as ActivationLike } from '@kui-shell/plugin-wskflow'
import success from '../../lib/success'
import { getPipelineFromRef, getTasks } from '../fetch'
import { Pipeline, PipelineRun, Task, TaskRef } from '../resource'
const strings = i18n('plugin-tekton', 'modes')
const debug = Debug('plugins/tekton/models/modes/trace')
interface RenderOpts {
noPip?: boolean
noCrop?: boolean
showStart?: boolean
showTimeline?: boolean
}
/**
* Render a trace view in the given container
*
*/
export const render = (tab: Tab, activations: ActivationLike[], container: Element, opts: RenderOpts = {}): void => {
const { noCrop = false, showStart = false, showTimeline = true } = opts
debug('trace', activations)
// add a legned
const legendHTMLtext = `<div class='legend-stripe'><div class='legend-entry' data-legend-type='queueing-delays' data-balloon='The time this activation waited for free execution resources' data-balloon-pos='left'>Queueing Delays<div class='legend-icon is-waitTime'></div></div><div class='legend-entry' data-legend-type='container-initialization' data-balloon='The "cold start time", i.e. time spent initializing a container' data-balloon-pos='left'>Container Initialization<div class='legend-icon is-initTime'></div></div><div class='legend-entry' data-legend-type='execution-time' data-balloon='The time this activation spent executing your code' data-balloon-pos='left'>Execution Time<div class='legend-icon is-runTime'></div></div><div class='legend-entry' data-legend-type='failures' data-balloon='The activation failed to complete' data-balloon-pos='left'>Failures<div class='legend-icon is-success-false'></div></div></div>`
const legend = document.createElement('div')
container.appendChild(legend)
legend.className = 'legend-trace legend-list'
legend.innerHTML = legendHTMLtext
const logTable = document.createElement('table')
logTable.className = 'log-lines log-lines-loose'
container.appendChild(logTable)
// duration of the activation. this will be helpful for
// normalizing the bar dimensions
const first = 0
const start = activations[first].start
const maxEnd = activations.reduce((max, activation) => Math.max(max, activation.end || activation.start + 1), 0) // the last one in the list might not have the highest end
const dur = Math.max(1, maxEnd - start, maxEnd - start)
const tgap = 0
const gaps: number[] = new Array(activations.length).fill(0)
const normalize = (value, idx) => {
// console.error(value, value-start, gaps[idx], value-start-gaps[idx], dur-tgap, (value - start - gaps[idx]) / (dur - tgap))
return (value - start - gaps[idx]) / (dur - tgap)
}
/* if (!entity) {
let residualDur = dur // after subtracing out gaps
for (let idx = activations.length - 2; idx >= 0; idx--) {
const activation = activations[idx]
const previous = activations[idx + 1]
const gap = activation.start - findItemInAnnotations('waitTime', activation) - (previous.end || (previous.start + 1))
if (gap > 0) {
const ngap = gap / residualDur
if (gap > 10000 || ngap > 0.05) {
tgap += gap
residualDur -= gap
for (let ii = idx; ii >= 0; ii--) {
gaps[ii] = gaps[ii] + gap
}
}
}
}
} */
activations.forEach((activation, idx) => {
//
// in this block, we are rendering a row for one activation
//
// if statusCode is undefined, check activation.response for success/fail info
// need to avoid isSuccess is set to undefined, as (false || undefined) returns undefined
// and re: statusCode === 0, see the note just above
const isSuccess = !activation.end
? true // rules and triggers. always successful?
: activation.statusCode !== undefined
? activation.statusCode === 0
: activation.response && activation.response.success
// row dom
const line = logTable.insertRow(-1)
line.className = 'log-line entity'
line.classList.add('activation')
line.setAttribute('data-name', activation.name)
if (idx === 0) line.classList.add('log-line-root')
const nextCell = () => line.insertCell(-1)
// column 1: activationId cell
const id = nextCell()
const clicky = document.createElement('span') as HTMLElement
clicky.className = 'clickable'
id.appendChild(clicky)
id.className = 'log-field'
if (noCrop) id.classList.add('full-width')
clicky.innerText = activation.activationId
id.setAttribute('data-activation-id', id.innerText)
// clicky.onclick = pip(show(activation))
// column 2: name cell
const name = nextCell()
const nameClick = document.createElement('span') as HTMLElement
name.className = 'slightly-deemphasize log-field entity-name'
nameClick.className = 'clickable'
nameClick.innerText = activation.name
name.appendChild(nameClick)
// command to be executed when clicking on the entity name cell
/* const path = activation.annotations && activation.annotations.find(({ key }) => key === 'path')
const gridCommand = activation.sessionId
? `grid ${repl.encodeComponent(activation.name)}` // for apps, the activation.name field is the app name
: !path ? `grid ${repl.encodeComponent(`/${activation.namespace}/${activation.name}`)}` // triggers, at least, have no path annotation
: `grid ${repl.encodeComponent(`/${path.value}`)}`
nameClick.onclick = pip(gridCommand) */
// column 3: duration cell
const duration = nextCell()
duration.className = 'somewhat-smaller-text log-field log-field-right-align duration-field'
duration.classList.add(isSuccess ? 'green-text' : 'red-text')
if (activation.end) {
duration.innerText = prettyPrintDuration(activation.end - activation.start)
} else {
// for trigger and rule, set duration to be 1ms. If duration is not set, qtip will show 'lasting undefined'
duration.innerText = prettyPrintDuration(1)
}
// column 4: success cell
/* const success = nextCell()
success.className = 'smaller-text lighter-text log-field success-field very-narrow'
empty(success)
const successBadge = document.createElement('badge')
successBadge.classList.add(isSuccess ? TrafficLight.Green : TrafficLight.Red)
successBadge.innerText = isSuccess ? 'OK' : 'Failed'
success.appendChild(successBadge) */
// queueing delays and container initialization time
const waitTime = 0
const initTime = 0
// column 5|6|7: bar chart cell
if (showTimeline) {
const timeline = nextCell()
empty(timeline)
const isRootBar = idx === 0
timeline.className = 'log-field log-line-bar-field'
// execution time bar
const bar = document.createElement('div') as HTMLElement
bar.style.position = 'absolute'
bar.classList.add('log-line-bar')
bar.classList.add(`is-success-${isSuccess}`)
const left = normalize(activation.start + initTime, idx)
const right = normalize(idx === 0 ? maxEnd : activation.end || activation.start + initTime + 1, idx) // handle rules and triggers as having dur=1
const width = right - left
// on which side should we render the tooltip?
const balloonPos = right > 0.9 ? 'left' : 'right'
bar.style.left = 100 * left + '%'
bar.style.width = 100 * width + '%'
// bar.onclick = pip(show(activation))
bar.setAttribute(
'data-balloon',
prettyPrintDuration(activation.end ? activation.end - activation.start - initTime : initTime)
)
bar.setAttribute('data-balloon-pos', balloonPos)
bar.onmouseover = () => legend.setAttribute('data-hover-type', isSuccess ? 'execution-time' : 'failures')
bar.onmouseout = () => legend.removeAttribute('data-hover-type')
// container initialization bar
let initTimeBar
let waitTimeBar
if (initTime > 0 && !isRootBar) {
initTimeBar = document.createElement('div')
const l = normalize(activation.start, idx)
const w = normalize(activation.start + initTime, idx) - l
initTimeBar.style.left = 100 * l + '%'
initTimeBar.style.width = 100 * w + '%'
initTimeBar.style.position = 'absolute'
initTimeBar.classList.add('log-line-bar')
initTimeBar.classList.add('is-initTime')
initTimeBar.onmouseover = () => legend.setAttribute('data-hover-type', 'container-initialization')
initTimeBar.onmouseout = () => legend.removeAttribute('data-hover-type')
// activation can fail at init time - if that's the case, initTime === duration
if (initTime === activation.duration) {
initTimeBar.classList.add(`is-success-false`)
} else {
initTimeBar.classList.add(`is-success-true`)
}
// initTimeBar.onclick = pip(show(activation))
initTimeBar.setAttribute('data-balloon', prettyPrintDuration(initTime))
initTimeBar.setAttribute('data-balloon-pos', balloonPos)
}
// queueing delays bar
if (waitTime > 0 && !isRootBar) {
waitTimeBar = document.createElement('div')
const l = normalize(activation.start - waitTime, idx)
const w = normalize(activation.start, idx) - l
waitTimeBar.style.left = 100 * l + '%'
waitTimeBar.style.width = 100 * w + '%'
waitTimeBar.style.position = 'absolute'
waitTimeBar.classList.add('log-line-bar')
waitTimeBar.classList.add('is-waitTime')
// waitTimeBar.onclick = pip(show(activation))
waitTimeBar.setAttribute('data-balloon', prettyPrintDuration(waitTime))
waitTimeBar.setAttribute('data-balloon-pos', balloonPos)
waitTimeBar.onmouseover = () => legend.setAttribute('data-hover-type', 'queueing-delays')
waitTimeBar.onmouseout = () => legend.removeAttribute('data-hover-type')
}
// here, we have to be careful to stack the bars in an order so that the tooltips will stack on top
// see shell issue #168
if (balloonPos === 'right') {
timeline.appendChild(bar)
if (initTimeBar) timeline.appendChild(initTimeBar)
if (waitTimeBar) timeline.appendChild(waitTimeBar)
} else {
if (waitTimeBar) timeline.appendChild(waitTimeBar)
if (initTimeBar) timeline.appendChild(initTimeBar)
timeline.appendChild(bar)
}
} // now we're done rendering the timeline bars
// column n: start cell
if (showStart) {
const start = nextCell()
const startInner = document.createElement('span') as HTMLElement
const previous = activations[idx - 1]
const previousWaitTime = 0
const previousStart = previous && previous.start - previousWaitTime
const time = prettyPrintTime(activation.start - waitTime, 'short', previousStart)
start.className =
'somewhat-smaller-text lighter-text log-field log-field-right-align start-time-field timestamp-like'
start.appendChild(startInner)
if (typeof time === 'string') {
startInner.innerText = time
} else {
empty(startInner)
startInner.appendChild(time)
}
}
})
}
function makeRunActivationLike(run: PipelineRun): ActivationLike {
const start = run && run.status && run.status.startTime && new Date(run.status.startTime)
const end = run && run.status && run.status.completionTime && new Date(run.status.completionTime)
const duration = start && end && end.getTime() - start.getTime()
return {
activationId: run.metadata.name,
name: run.spec.pipelineRef.name,
start: start && start.getTime(),
end: end && end.getTime(),
duration,
response: {
success: success(run.status.conditions)
}
}
}
interface SymbolTable<N> {
[key: string]: N
}
function makeSymbolTables(pipeline: Pipeline, jsons: KubeResource[]) {
// map from Task.metadata.name to Task
const taskName2Task: SymbolTable<Task> = jsons
.filter(_ => _.kind === 'Task')
.reduce((symtab: SymbolTable<Task>, task: Task) => {
symtab[task.metadata.name] = task
return symtab
}, {})
// map from Pipeline.Task.name to Task
const taskRefName2Task: SymbolTable<Task> = pipeline.spec.tasks.reduce(
(symtab: SymbolTable<Task>, taskRef: TaskRef) => {
symtab[taskRef.name] = taskName2Task[taskRef.taskRef.name]
return symtab
},
{}
)
return { taskRefName2Task }
}
function makeTaskRunsActivationLike(run: PipelineRun, pipeline: Pipeline, jsons: KubeResource[]): ActivationLike[] {
const runs = run && run.status.taskRuns
const { taskRefName2Task } = makeSymbolTables(pipeline, jsons)
const activations = Object.keys(runs || []).reduce((M: ActivationLike[], _: string) => {
const taskRun = runs[_]
const taskRefName = taskRun.pipelineTaskName
const task = taskRefName2Task[taskRefName]
if (!task) {
console.error('!! task not found', taskRefName, taskRefName2Task)
} else {
/* const start = new Date(taskRun.status.startTime).getTime()
task.visitedIdx = M.length
M.push({
start,
duration: taskRun.status.completionTime ? new Date(taskRun.status.completionTime).getTime() - start : 0,
response: {
success: success(taskRun.status.conditions)
}
}) */
taskRun.status.steps.forEach(stepRun => {
const start = new Date(stepRun.terminated.startedAt).getTime()
const end = new Date(stepRun.terminated.finishedAt).getTime()
const success = stepRun.terminated.reason !== 'Error'
/* const step = task.spec.steps.find(_ => _.name === stepRun.name)
if (!step) {
console.error('!! step not found', stepRun.name, task.spec.steps)
} else if (step) {
step.visitedIdx = M.length
} */
M.push({
activationId: taskRun.pipelineTaskName,
name: stepRun.name,
start,
end,
duration: end - start,
response: {
success
}
})
})
}
return M
}, [] as ActivationLike[])
activations.sort((a, b) => a.start - b.start)
return activations
}
export const traceView = (tab: Tab, run: PipelineRun, pipeline: Pipeline, jsons: KubeResource[]) => {
const content = document.createElement('div')
content.classList.add('padding-content', 'repl-result')
content.style.flex = '1'
content.style.display = 'flex'
content.style.flexDirection = 'column'
content.style.overflowX = 'hidden'
const runActivation = makeRunActivationLike(run)
render(tab, [runActivation].concat(makeTaskRunsActivationLike(run, pipeline, jsons)), content)
const badges: Badge[] = ['Tekton']
return {
type: 'custom',
isEntity: true,
name: run.metadata.name,
packageName: run.metadata.namespace,
prettyType: 'PipelineRun',
duration: runActivation.duration,
badges,
content
}
}
/**
* Sidecar mode for a pipeline run trace view
*
*/
const traceMode: Mode = {
mode: 'trace',
label: strings('trace'),
content: async (tab: Tab, resource: PipelineRun) => {
const [pipeline, tasks] = await Promise.all([getPipelineFromRef(tab, resource), getTasks(tab)])
return traceView(tab, resource, pipeline, tasks)
},
defaultMode: true
}
export default traceMode