Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 47872f4

Browse files
myan9starpit
authored andcommitted
feat: add Retry button to sidecar toolbar when log streaming stopped abnormally
Fixes #4755
1 parent cc59503 commit 47872f4

File tree

10 files changed

+302
-128
lines changed

10 files changed

+302
-128
lines changed

plugins/plugin-client-common/src/components/spi/Icons/impl/Carbon.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
List16 as List,
4545
PauseOutlineFilled16 as Pause,
4646
PlayFilled16 as Play,
47+
Renew16 as Retry,
4748
CaretRight20 as NextPage,
4849
CaretLeft20 as PreviousPage,
4950
FlashOffFilled20 as Network
@@ -67,6 +68,7 @@ const icons: Record<Exclude<SupportedIcon, 'Up'>, CarbonIconType> = {
6768
NextPage,
6869
Pause,
6970
Play,
71+
Retry,
7072
PreviousPage,
7173
Screenshot,
7274
ScreenshotInProgress,

plugins/plugin-client-common/src/components/spi/Icons/impl/PatternFly.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
CaretRightIcon as NextPage,
4444
NetworkWiredIcon as Network,
4545
PauseCircleIcon as Pause,
46+
RebootingIcon as Retry,
4647
PlayCircleIcon as Play
4748
} from '@patternfly/react-icons'
4849

@@ -86,6 +87,8 @@ export default function PatternFly4Icons(props: Props) {
8687
return <Pause {...props} />
8788
case 'Play':
8889
return <Play {...props} />
90+
case 'Retry':
91+
return <Retry {...props} />
8992
case 'PreviousPage':
9093
return <PreviousPage style={Pagination} {...props} />
9194
case 'Network':

plugins/plugin-client-common/src/components/spi/Icons/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type SupportedIcon =
3535
| 'NextPage'
3636
| 'Pause'
3737
| 'Play'
38+
| 'Retry'
3839
| 'PreviousPage'
3940
| 'Screenshot'
4041
| 'ScreenshotInProgress'

plugins/plugin-kubectl/i18n/logs_en_US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"Log streaming stopped abnormally.": "Log streaming **stopped abnormally**.",
2020
"Pause Streaming": "Pause Streaming",
2121
"Resume Streaming": "Resume Streaming",
22+
"Retry": "Retry",
2223
"Occurred at": "Occurred at {0}",
2324
"Stack Trace": "Stack Trace",
2425
"Error": "Error"

plugins/plugin-kubectl/logs/src/test/logs/logs-dash-c.ts

Lines changed: 130 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -58,57 +58,105 @@ wdescribe(`kubectl Logs tab ${process.env.MOCHA_RUN_TARGET || ''}`, function(thi
5858
const containerName1 = 'nginx'
5959
const containerName2 = 'vim'
6060

61-
it(`should create sample pod from URL`, () => {
62-
return CLI.command(`echo ${inputEncoded} | base64 --decode | kubectl create -f - -n ${ns}`, this.app)
63-
.then(ReplExpect.okWithPtyOutput(podName))
64-
.catch(Common.oops(this, true))
65-
})
61+
const createPodWithoutWaiting = () => {
62+
it(`should create sample pod from URL`, () => {
63+
return CLI.command(`echo ${inputEncoded} | base64 --decode | kubectl create -f - -n ${ns}`, this.app)
64+
.then(ReplExpect.okWithPtyOutput(podName))
65+
.catch(Common.oops(this, true))
66+
})
67+
}
6668

67-
it(`should wait for the pod to come up`, () => {
68-
return CLI.command(`kubectl get pod ${podName} -n ${ns} -w`, this.app)
69-
.then(ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(podName) }))
70-
.then(selector => waitForGreen(this.app, selector))
71-
.catch(Common.oops(this, true))
72-
})
69+
const waitForPod = () => {
70+
it(`should wait for the pod to come up`, () => {
71+
return CLI.command(`kubectl get pod ${podName} -n ${ns} -w`, this.app)
72+
.then(ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(podName) }))
73+
.then(selector => waitForGreen(this.app, selector))
74+
.catch(Common.oops(this, true))
75+
})
76+
}
7377

74-
it(`should get pods via kubectl then click`, async () => {
75-
try {
76-
const selector: string = await CLI.command(`kubectl get pods ${podName} -n ${ns}`, this.app).then(
77-
ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(podName) })
78-
)
79-
80-
// wait for the badge to become green
81-
await waitForGreen(this.app, selector)
82-
83-
// now click on the table row
84-
await this.app.client.click(`${selector} .clickable`)
85-
await SidecarExpect.open(this.app)
86-
.then(SidecarExpect.mode(defaultModeForGet))
87-
.then(SidecarExpect.showing(podName))
88-
} catch (err) {
89-
return Common.oops(this, true)(err)
90-
}
91-
})
78+
const getPodViaClick = (wait = true) => {
79+
it(`should get pods via kubectl then click`, async () => {
80+
try {
81+
const selector: string = await CLI.command(`kubectl get pods ${podName} -n ${ns}`, this.app).then(
82+
ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(podName) })
83+
)
9284

93-
it('should show logs tab', async () => {
94-
try {
95-
await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON('logs'))
96-
await this.app.client.click(Selectors.SIDECAR_MODE_BUTTON('logs'))
97-
await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON_SELECTED('logs'))
85+
if (wait) {
86+
// wait for the badge to become green
87+
await waitForGreen(this.app, selector)
88+
}
9889

99-
await SidecarExpect.toolbarText({ type: 'info', text: 'Logs are live', exact: false })(this.app)
90+
// now click on the table row
91+
await this.app.client.click(`${selector} .clickable`)
92+
await SidecarExpect.open(this.app)
93+
.then(SidecarExpect.mode(defaultModeForGet))
94+
.then(SidecarExpect.showing(podName))
95+
} catch (err) {
96+
return Common.oops(this, true)(err)
97+
}
98+
})
99+
}
100100

101-
await sleep(sleepTime)
102-
await waitForLogText((text: string) => text.includes(containerName1) && text.includes(containerName2))
103-
} catch (err) {
104-
return Common.oops(this, true)(err)
101+
const getPodViaYaml = () => {
102+
it('should get pods via kubectl get -o yaml', async () => {
103+
try {
104+
await CLI.command(`kubectl get pods ${podName} -n ${ns} -o yaml`, this.app)
105+
await SidecarExpect.open(this.app)
106+
} catch (err) {
107+
return Common.oops(this, true)(err)
108+
}
109+
})
110+
}
111+
112+
const testLogsContent = (show: string[], notShow?: string[]) => {
113+
if (show) {
114+
show.forEach(showInLog => {
115+
it(`should show ${showInLog} in log output`, async () => {
116+
try {
117+
await sleep(sleepTime)
118+
await waitForLogText((text: string) => text.indexOf(showInLog) !== -1)
119+
} catch (err) {
120+
return Common.oops(this, true)(err)
121+
}
122+
})
123+
})
105124
}
106-
})
125+
126+
if (notShow) {
127+
notShow.forEach(notShowInLog => {
128+
it(`should not show ${notShowInLog} in log output`, async () => {
129+
try {
130+
await sleep(sleepTime)
131+
await waitForLogText((text: string) => text.indexOf(notShowInLog) === -1)
132+
} catch (err) {
133+
return Common.oops(this, true)(err)
134+
}
135+
})
136+
})
137+
}
138+
}
139+
140+
const switchToLogsTab = (showInLog: string[], toolbar: { text: string; type: string }) => {
141+
it('should show logs tab', async () => {
142+
try {
143+
await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON('logs'))
144+
await this.app.client.click(Selectors.SIDECAR_MODE_BUTTON('logs'))
145+
await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON_SELECTED('logs'))
146+
147+
await SidecarExpect.toolbarText({ type: toolbar.type, text: toolbar.text, exact: false })(this.app)
148+
149+
testLogsContent(showInLog)
150+
} catch (err) {
151+
return Common.oops(this, true)(err)
152+
}
153+
})
154+
}
107155

108156
const switchContainer = (
109157
container: string,
110-
_showInLog: string[],
111-
_notShowInLog: string[],
158+
showInLog: string[],
159+
notShowInLog: string[],
112160
toolbar: { text: string; type: string }
113161
) => {
114162
it(`should switch to container ${container}`, async () => {
@@ -124,27 +172,7 @@ wdescribe(`kubectl Logs tab ${process.env.MOCHA_RUN_TARGET || ''}`, function(thi
124172
}
125173
})
126174

127-
_showInLog.forEach(showInLog => {
128-
it(`should show ${showInLog} in log output`, async () => {
129-
try {
130-
await sleep(sleepTime)
131-
await waitForLogText((text: string) => text.indexOf(showInLog) !== -1)
132-
} catch (err) {
133-
return Common.oops(this, true)(err)
134-
}
135-
})
136-
})
137-
138-
_notShowInLog.forEach(notShowInLog => {
139-
it(`should not show ${notShowInLog} in log output`, async () => {
140-
try {
141-
await sleep(sleepTime)
142-
await waitForLogText((text: string) => text.indexOf(notShowInLog) === -1)
143-
} catch (err) {
144-
return Common.oops(this, true)(err)
145-
}
146-
})
147-
})
175+
testLogsContent(showInLog, notShowInLog)
148176
}
149177

150178
const toggleStreaming = (changeToLive: boolean) => {
@@ -164,6 +192,25 @@ wdescribe(`kubectl Logs tab ${process.env.MOCHA_RUN_TARGET || ''}`, function(thi
164192
})
165193
}
166194

195+
const doRetry = (showInLog: string[], toolbar: { text: string; type: string }) => {
196+
it('should hit retry', async () => {
197+
try {
198+
await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON('retry-streaming'))
199+
await this.app.client.click(Selectors.SIDECAR_MODE_BUTTON('retry-streaming'))
200+
await SidecarExpect.toolbarText({ text: toolbar.text, type: toolbar.type, exact: false })(this.app)
201+
testLogsContent(showInLog)
202+
} catch (err) {
203+
return Common.oops(this, true)(err)
204+
}
205+
})
206+
}
207+
208+
/* Here comes the test */
209+
createPodWithoutWaiting()
210+
waitForPod()
211+
getPodViaClick()
212+
switchToLogsTab([containerName1, containerName2], { text: 'Logs are live', type: 'info' })
213+
167214
/** testing various combination here */
168215
switchContainer(containerName1, [containerName1], [containerName2], { text: containerName1, type: 'info' })
169216

@@ -237,5 +284,25 @@ wdescribe(`kubectl Logs tab ${process.env.MOCHA_RUN_TARGET || ''}`, function(thi
237284
type: 'error'
238285
})
239286

287+
doRetry(['not found'], {
288+
text: showError,
289+
type: 'error'
290+
})
291+
292+
createPodWithoutWaiting() // recreate this pod
293+
getPodViaYaml() // NOTE: immediately open sidecar when pod is in creation
294+
295+
switchToLogsTab(['not found'], {
296+
text: showError,
297+
type: 'error'
298+
})
299+
300+
waitForPod() // wait for pod ready
301+
302+
doRetry([containerName1, containerName2], {
303+
text: 'Logs are live',
304+
type: 'info'
305+
})
306+
240307
deleteNS(this, ns)
241308
})

plugins/plugin-kubectl/src/lib/view/modes/ContainerCommon.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import * as React from 'react'
18-
import { DropDown } from '@kui-shell/plugin-client-common'
18+
import { DropDown, Icons } from '@kui-shell/plugin-client-common'
1919
import { Abortable, Arguments, Button, FlowControllable, Tab, ToolbarProps, ToolbarText, i18n } from '@kui-shell/core'
2020

2121
import { Pod } from '../../model/resource'
@@ -63,10 +63,31 @@ export abstract class ContainerComponent<State extends ContainerState> extends R
6363
> {
6464
protected abstract toolbarText(status: StreamingStatus): ToolbarText
6565

66+
protected toolbarButtonsForError(status: StreamingStatus): Button[] {
67+
if (status === 'Error') {
68+
return [
69+
{
70+
mode: 'retry-streaming',
71+
label: strings('Retry'),
72+
kind: 'view',
73+
icon: <Icons icon="Retry" onClick={() => this.showContainer(this.state.container)} />,
74+
command: () => {} // eslint-disable-line @typescript-eslint/no-empty-function
75+
} as Button
76+
]
77+
} else {
78+
return []
79+
}
80+
}
81+
82+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
83+
protected toolbarButtonsForStreaming(status: StreamingStatus): Button[] {
84+
return []
85+
}
86+
6687
/** Buttons to display in the Toolbar. */
6788
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6889
protected toolbarButtons(status: StreamingStatus): Button[] {
69-
return this.containerList()
90+
return this.toolbarButtonsForError(status).concat(this.toolbarButtonsForStreaming(status), this.containerList())
7091
}
7192

7293
protected supportsAllContainers() {

plugins/plugin-kubectl/src/lib/view/modes/Events.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,23 @@ class Events extends React.PureComponent<Props, State> {
168168
}
169169
}
170170

171-
/** Toolbar buttons to display */
172-
protected toolbarButtons(status: StreamingStatus) {
171+
protected toolbarButtonsForError(status: StreamingStatus): Button[] {
172+
if (status === 'Error') {
173+
return [
174+
{
175+
mode: 'retry-streaming',
176+
label: strings('Retry'),
177+
kind: 'view',
178+
icon: <Icons icon="Retry" onClick={() => this.initStream()} />,
179+
command: () => {} // eslint-disable-line @typescript-eslint/no-empty-function
180+
} as Button
181+
]
182+
} else {
183+
return []
184+
}
185+
}
186+
187+
protected toolbarButtonsForStreaming(status: StreamingStatus): Button[] {
173188
const isLive = status === 'Live'
174189
return [
175190
{
@@ -182,6 +197,11 @@ class Events extends React.PureComponent<Props, State> {
182197
]
183198
}
184199

200+
/** Toolbar buttons to display */
201+
protected toolbarButtons(status: StreamingStatus) {
202+
return this.toolbarButtonsForError(status).concat(this.toolbarButtonsForStreaming(status))
203+
}
204+
185205
/** Update Toolbar text and Toolbar buttons. */
186206
protected updateToolbar(status: StreamingStatus) {
187207
this.props.toolbarController.willUpdateToolbar(

plugins/plugin-kubectl/src/lib/view/modes/ExecIntoPod.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class Terminal<S extends TerminalState = TerminalState> extends Container
106106
super(props)
107107

108108
this.state = {
109-
container: props.pod.spec.containers[0].name,
109+
container: this.defaultContainer(),
110110

111111
dom: undefined,
112112
xterm: undefined,
@@ -159,6 +159,10 @@ export class Terminal<S extends TerminalState = TerminalState> extends Container
159159
}
160160
}
161161

162+
protected defaultContainer() {
163+
return this.props.pod.spec.containers[0].name
164+
}
165+
162166
/**
163167
* Convert the current theme to an xterm.js ITheme
164168
*
@@ -295,6 +299,8 @@ export class Terminal<S extends TerminalState = TerminalState> extends Container
295299
const { pod } = this.props
296300
const { container } = this.state
297301

302+
console.error('yoyo', container)
303+
298304
return `${getCommandFromArgs(this.props.args)} exec -it ${pod.metadata.name} -c ${container} -n ${
299305
pod.metadata.namespace
300306
} -- sh`
@@ -404,7 +410,7 @@ export class Terminal<S extends TerminalState = TerminalState> extends Container
404410
return {
405411
job: undefined,
406412
streamUUID: undefined,
407-
container: undefined,
413+
container: curState.container,
408414
isLive,
409415
isTerminated: true
410416
}

0 commit comments

Comments
 (0)