-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pod attach/exec proto #1939
Pod attach/exec proto #1939
Conversation
ab5eb75
to
45a478b
Compare
Codecov Report
@@ Coverage Diff @@
## master #1939 +/- ##
==========================================
- Coverage 60.5% 59.33% -1.17%
==========================================
Files 537 543 +6
Lines 11186 11445 +259
==========================================
+ Hits 6768 6791 +23
- Misses 4246 4482 +236
Partials 172 172
Continue to review full report at Codecov.
|
@ianlewis, @floreks, @maciaszczykm, @rf232, @bryk, @jwforres, @beyondblog, @Dmitry1987, if you have a some time your comments would be much appreciated. I hope I haven't left out anyone who seem to have shown interest previously.. Thanks |
I just tested it with |
Update: I'm working on adding a SockJS based transport to circumvent the lack of WebSocket support in the kubectl proxy. (SockJS is using WebSockets if possible and falling back on a number of different alternatives otherwise. Some of those can pass trough kubectl proxy fine.) It might be a good idea anyway as not all corp proxies fancy WebSockets. |
@lenartj Good idea, I am going to take a look on it after release too. |
Update: the SockJS implementation works fine. I will try to push the code tomorrow after some cleanup. |
I have quickly looked at the code and overall it looks fine. Probably we'll have some comments about where to place the code. You can continue for now. Once it's ready for review just write PTAL. |
@floreks, PTAL - also, I have updated the tasks in the first comment, please take a look. For everyone else: If you want to give this code a try you can do (k8s 1.6+): If you are running I have tested this using the following setups:
|
Sorry for the delay. I will take a look at it tomorrow. |
@floreks, would it help if I cut this PR into 2-3 separate PRs? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have few comments. Also please add some documentation. For all public functions in go we need to have documentation. It would be nice to have it also for private functions but if logic is simple then it is not required.
Mostly it looks nice. I've tested few cases and it works well. Thanks again for this contribution. It is really great!
podName := request.PathParameter("pod") | ||
containerName := request.PathParameter("container") | ||
|
||
req := getApiClient(request).Core().RESTClient().Post(). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this is related to pod I'd move this logic to i.e. attach.go
or terminal.go
file under pod resource. As this is actually exec then maybe we can keep it under some generic name like terminal/console.
src/app/backend/handler/attach.go
Outdated
return 0, err | ||
} | ||
|
||
var msg struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor structs outside of the functions.
src/app/backend/handler/attach.go
Outdated
var attachSessions = make(map[string]AttachSession) | ||
|
||
func handleAttachSession(session sockjs.Session) { | ||
if buf, err := session.Recv(); err == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd reverse the logic here to decrease complexity.
if ...; err != nil {
log(err)
return
}
Also in case of any error we can probably just retun immediately, then last part does not have to be in else block.
// https://github.com/sockjs/sockjs-client | ||
|
||
this.term = new hterm.Terminal(); | ||
this.term.onTerminalReady = function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd extract anonymous functions to class functions. It will be easier to document and understand the code.
By the way instead of function() {}
you can use ES6 fat arrow syntax () => {}
.
@maciaszczykm could you also take a closer look at it? I may have missed something. |
Very useful comments. I will get to it in the coming few days. |
@floreks, PTAL. Read the commit message for info. Mostly just cosmetical changes but touches a lot of lines. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works okay, but I have few comments regarding code itself. We can chat on Slack if something is not clear.
@@ -19,9 +19,8 @@ import browserSync from 'browser-sync'; | |||
import browserSyncSpa from 'browser-sync-spa'; | |||
import child from 'child_process'; | |||
import gulp from 'gulp'; | |||
import proxyMiddleware from 'http-proxy-middleware'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should run gulp format
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -95,6 +96,10 @@ type CsrfToken struct { | |||
Token string `json:"token"` | |||
} | |||
|
|||
type TerminalResponse struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add comments on big-case types and functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/app/backend/handler/terminal.go
Outdated
|
||
// Called from apihandler.handleAttach as a goroutine | ||
// Waits for the SockJS connection to be opened by the client the session to be bound in handleTerminalSession | ||
func TerminalWaiter(request *restful.Request, sessionId string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function name should be verb, not a noun.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/app/backend/handler/terminal.go
Outdated
"k8s.io/kubernetes/pkg/api" | ||
) | ||
|
||
// What remotecommand expects from a pty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please start comments with type/method name. In this case PtyHandler ...
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/app/backend/handler/terminal.go
Outdated
} | ||
|
||
type TerminalMessage struct { | ||
Op, Data, SessionID string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain it a little? What is Op
etc.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/app/backend/handler/terminal.go
Outdated
// Called from main for /api/sockjs | ||
func CreateAttachHandler(path string) (http.Handler) { | ||
options := sockjs.DefaultOptions | ||
// options.Websocket = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if if you are asking about options
or the whole function. I've removed the variable; it was just a leftover from experiments.
If you meant the function itself: I think it's a good idea to abstract this implementation detail out of the main package (dashboard.go). It is also how the other handlers are setup:
http.Handle("/", handler.MakeGzipHandler(handler.CreateLocaleHandler())) http.Handle("/api/", apiHandler) // TODO(maciaszczykm): Move to /appConfig.json as it was discussed in #640. http.Handle("/api/appConfig.json", handler.AppHandler(handler.ConfigHandler)) http.Handle("/api/sockjs/", handler.CreateAttachHandler("/api/sockjs")) http.Handle("/metrics", prometheus.Handler())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just about the single line :-) It's good.
src/app/backend/handler/terminal.go
Outdated
} | ||
|
||
// Checks if the shell is an allowed one | ||
func isValidShell(validShells, shell []string) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is shell an array? Before call you do []string{request.QueryParameter("shell")}
which is redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am thinking of commands like tmux att
. Maybe over-engineered?
src/app/backend/handler/terminal.go
Outdated
// Called from apihandler.handleAttach as a goroutine | ||
// Waits for the SockJS connection to be opened by the client the session to be bound in handleTerminalSession | ||
func TerminalWaiter(request *restful.Request, sessionId string) { | ||
shell := []string{request.QueryParameter("shell")} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use shell := request.QueryParameter("shell")
instead.
src/app/backend/handler/terminal.go
Outdated
|
||
if shell[0] != "" { | ||
if isValidShell(validShells, shell) { | ||
err = execHelper(request, shell, terminalSessions[sessionId]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need to pass an array do it here instead of using array everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, use different name, because using shell
everywhere is confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes using shell
in the loop as well is pretty confusing. I've changed the latter.. Is it any better?
src/app/backend/handler/terminal.go
Outdated
var err error | ||
validShells := []string{"bash", "sh"} | ||
|
||
if shell[0] != "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of doing this check check if isValidShell(validShells, shell)
, else try some shells until one succeeds or all fail
. Then, we don't need to throw err = fmt.Errorf("Invalid shell")
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
I've experimented with a couple of different paddings:
All in all, I am not a fan of any of the paddings above so I haven't added any for now. |
I've removed the padding from the left. It turns out that the width of the black vertical bar on the right depends on the exact the width of the box. Something like: rightpaddingwidth = totalwidth%charwidth, so it can be 0 depending on the width. |
FYI: the old one, I almost forgot https://github.com/kubernetes-ui/container-terminal |
// Can happen if the process exits or if there is an error starting up the process | ||
// For now the status code is unused and reason is shown to the user (unless "") | ||
func (t TerminalSession) Close(status uint32, reason string) { | ||
t.sockJSSession.Close(status, reason) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
session.Close()
can return an error which is currently unhandled, crashing the dashboard. Might be good for this Close function to pass the error down to usages if it's not nil, like in line 261 and 265.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this could/should be done, but it won't fix a crash. The crash happens when a Read() happens on the closed session. I will look into this, thanks
I think you have missed @maciaszczykm comment. Also you need to rebase code. Rest LGTM. Last changes and we are ready to merge. |
@maciaszczykm, @floreks, I must be blind, I can't find the comment I've missed (I have followed the link too) Do you mean this one?
But to this I have replied/explained and have received no further comment (or maybe just can't find it?) |
hmm... I don't see any reply to this comment. Link is valid and I can see a comment from @maciaszczykm (scroll a bit up if you can't see it). |
I may also missed one of the replies, but generally |
Loud and clear. I fix that. |
- github.com/gorilla/websocket - gopkg.in/igm/sockjs-go.v2/sockjs - k8s.io/kubernetes/pkg/client/unversioned/remotecommand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. We can improve it in subsequent PRs.
@lenartj Thanks for your great feature! I deployed it in k8s 1.6.1 with And when I want to exec in the container "kubernetes-dashboard-head" itself, an error occurred and the dashboard crashed and restarted. Is that expected? Dashboard crashes when an oci runtime error occurs. Maybe this error should be handled. |
Not every container supports exec into it. It depends on what base image it was created. Dashboard does not support it. When you try to exec into it, it will crash. Sent from my LGE Pixel using FastHub |
Dashboard container has no shell and as @floreks explained exec into it will not work. However, the running Dashboard instance should not crash and it isn't in my environment. Are you using roket? Anything else special in your environment? |
Sorry I only noticed this comment now. I will be able to take a look on Wednesday, I am busy until that :/ |
I'm just using docker 1.12.6, nothing special |
Hmm, it seems that the dashboard pod will exit with error if you're executing a non-existing command. |
This is a different issue related to session handling. We are working on a fix. |
Prototype -- please comment extensively. This is building on code/ideas/comments from #1455 and #1345, so some of the problems/suggestions mentioned there are addressed.
Task list:
bash
thansh
for now)*attach*
functions and structures. Dependencies for gorilla/websocket should be removed.SockJSURL
from dashboardI have been running it like this:
DOCKER_RUN_OPTS="-v $HOME/.kube/config:/kubeconfig.yaml:ro -e KUBE_DASHBOARD_KUBECONFIG=/kubeconfig.yaml" build/run-gulp-in-docker.sh serve