Skip to content
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

Merged
merged 2 commits into from
Jun 16, 2017
Merged

Pod attach/exec proto #1939

merged 2 commits into from
Jun 16, 2017

Conversation

lenartj
Copy link
Contributor

@lenartj lenartj commented May 12, 2017

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:

  • Builds prod
  • You can actually execute a shell and get a terminal in the containers
  • Support trying different shells (tries bash than sh for now)
  • Using hterm. So copy-paste, colors, mouse, etc.
  • Resizing
  • Moon buggy!!
  • Message on disconnect
  • SockJS transport (kubectl proxy does not support websockets ("kubectl exec" does not work through "kubectl proxy" kubernetes#32026) which is a major use case. So if websockets do not work it falls back on different kinds of http streaming. It is jerkier and much less efficient than websockets but at least it works.)
  • Ability to attach as in kubectl attach
  • Come up with a better name than "Attach" for the shell exec link
  • Make code look not horrible. For example, find better names for all the *attach* functions and structures. Dependencies for gorilla/websocket should be removed.
  • Implement locking for session map (while the current code is bad, actually triggering the problem is unlikely)
  • Add links leading to exec view (something like logs button)
  • Serve SockJSURL from dashboard
  • utf-8 input (needs hterm needs configuration)
  • Add some tests
  • Some solution to respawn if the user exits the shell (?)
  • Some padding on the left (?)

I 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

moon-buggy

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label May 12, 2017
@lenartj lenartj force-pushed the attach-proto branch 5 times, most recently from ab5eb75 to 45a478b Compare May 14, 2017 17:35
@codecov-io
Copy link

codecov-io commented May 14, 2017

Codecov Report

Merging #1939 into master will decrease coverage by 1.16%.
The diff coverage is 8.88%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
src/app/backend/handler/terminal.go 0% <0%> (ø)
src/app/frontend/shell/module.js 100% <100%> (ø)
src/app/frontend/index_module.js 100% <100%> (ø) ⬆️
src/app/backend/handler/apihandler.go 26.23% <14.81%> (-0.18%) ⬇️
src/app/frontend/shell/termopen_directive.js 25% <25%> (ø)
src/app/frontend/shell/controller.js 5.76% <5.76%> (ø)
...app/frontend/pod/detail/containerinfo_component.js 84.61% <50%> (-6.3%) ⬇️
src/app/frontend/shell/stateconfig.js 57.14% <57.14%> (ø)
src/app/frontend/shell/state.js 75% <75%> (ø)
... and 5 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f534482...958ffc5. Read the comment docs.

@lenartj
Copy link
Contributor Author

lenartj commented May 15, 2017

@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

@cheld
Copy link
Contributor

cheld commented May 15, 2017

I just tested it with gulp serve and gulp serve:prod. Cool. The attach function is a bit hidden, but thats a detail... Had no chance yet to look at the code.

@lenartj
Copy link
Contributor Author

lenartj commented May 16, 2017

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.

@maciaszczykm
Copy link
Member

@lenartj Good idea, I am going to take a look on it after release too.

@lenartj
Copy link
Contributor Author

lenartj commented May 17, 2017

Update: the SockJS implementation works fine. I will try to push the code tomorrow after some cleanup.

@floreks
Copy link
Member

floreks commented May 18, 2017

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.

@lenartj
Copy link
Contributor Author

lenartj commented May 20, 2017

@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+):
kubectl apply -f https://lenart.io/kubernetes-dashboard-head-attach-proto.yaml

If you are running kubectl proxy visit the address http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/kubernetes-dashboard-head/proxy/

I have tested this using the following setups:

  • Chrome on Linux, via kubectl proxy (transport=xhr stream)
  • Chrome on Linux, trough an ingress (secured with mandatory client certificate; transport=websocket)
  • Chrome on Android (trough the ingress; transport=websocket)

@floreks
Copy link
Member

floreks commented May 24, 2017

Sorry for the delay. I will take a look at it tomorrow.

@lenartj
Copy link
Contributor Author

lenartj commented May 26, 2017

@floreks, would it help if I cut this PR into 2-3 separate PRs?

Copy link
Member

@floreks floreks left a 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().
Copy link
Member

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.

return 0, err
}

var msg struct {
Copy link
Member

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.

var attachSessions = make(map[string]AttachSession)

func handleAttachSession(session sockjs.Session) {
if buf, err := session.Recv(); err == nil {
Copy link
Member

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() {
Copy link
Member

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 () => {}.

@floreks
Copy link
Member

floreks commented May 26, 2017

@maciaszczykm could you also take a closer look at it? I may have missed something.

@lenartj
Copy link
Contributor Author

lenartj commented May 26, 2017

Very useful comments. I will get to it in the coming few days.

@lenartj
Copy link
Contributor Author

lenartj commented May 28, 2017

@floreks, PTAL. Read the commit message for info. Mostly just cosmetical changes but touches a lot of lines.

@maciaszczykm
Copy link
Member

maciaszczykm commented May 29, 2017

I have just checked out how it works and I really like it.

One minor thing for a quick fix related to styling:

image

We should apply padding here, just like in logs view:

image

I will take a look on the code soon.

@maciaszczykm
Copy link
Member

We should also add links leading to exec view (something like logs button), but we can work on it later on:

image

Copy link
Member

@maciaszczykm maciaszczykm left a 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';
Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// 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) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"k8s.io/kubernetes/pkg/api"
)

// What remotecommand expects from a pty
Copy link
Member

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 ....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

type TerminalMessage struct {
Op, Data, SessionID string
Copy link
Member

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.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// Called from main for /api/sockjs
func CreateAttachHandler(path string) (http.Handler) {
options := sockjs.DefaultOptions
// options.Websocket = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need it?

Copy link
Contributor Author

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())

Copy link
Member

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.

}

// Checks if the shell is an allowed one
func isValidShell(validShells, shell []string) bool {
Copy link
Member

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.

Copy link
Contributor Author

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?

// 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")}
Copy link
Member

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.


if shell[0] != "" {
if isValidShell(validShells, shell) {
err = execHelper(request, shell, terminalSessions[sessionId])
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

var err error
validShells := []string{"bash", "sh"}

if shell[0] != "" {
Copy link
Member

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").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@lenartj
Copy link
Contributor Author

lenartj commented May 29, 2017

I've experimented with a couple of different paddings:

  • Around: padding on the right of the scrollbar looks really odd
    image

  • padding-left only, black: the actual background color of hterm is not quite black, but #f0f0f0 so black looks odd next to it
    image

  • padding-left only, same as hterm
    image
    ... but now any process that changes the background color will have an almost black line next to it:
    image

  • padding-left only, color inherited (whiteish: this looks the least odd, but doesn't accomplish much in my opinion:
    image

All in all, I am not a fan of any of the paddings above so I haven't added any for now.

@lenartj lenartj closed this May 29, 2017
@lenartj
Copy link
Contributor Author

lenartj commented May 30, 2017

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.

@floreks floreks mentioned this pull request Jun 1, 2017
@danielromlein danielromlein added this to the 2017 roadmap milestone Jun 2, 2017
@danielromlein danielromlein mentioned this pull request Jun 2, 2017
4 tasks
@cheld
Copy link
Contributor

cheld commented Jun 2, 2017

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)

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.

Copy link
Contributor Author

@lenartj lenartj Jun 4, 2017

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

@floreks
Copy link
Member

floreks commented Jun 5, 2017

I think you have missed @maciaszczykm comment.

Also you need to rebase code. Rest LGTM. Last changes and we are ready to merge.

@lenartj
Copy link
Contributor Author

lenartj commented Jun 5, 2017

@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?

Use shell := request.QueryParameter("shell") instead.

But to this I have replied/explained and have received no further comment (or maybe just can't find it?)

@floreks
Copy link
Member

floreks commented Jun 5, 2017

Do you mean this one?

Use shell := request.QueryParameter("shell") instead.

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).

@maciaszczykm
Copy link
Member

I may also missed one of the replies, but generally shell := []string{request.QueryParameter("shell")} doesn't look good especially when in isValidShell you need to do shell[0]. In my opinion isValidShell should accept single string, not array and extract last one. You should change that. I think @floreks will agree with me.

@lenartj
Copy link
Contributor Author

lenartj commented Jun 5, 2017

Loud and clear. I fix that.

- github.com/gorilla/websocket
- gopkg.in/igm/sockjs-go.v2/sockjs
- k8s.io/kubernetes/pkg/client/unversioned/remotecommand
Copy link
Member

@floreks floreks left a 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.

@floreks floreks dismissed maciaszczykm’s stale review June 16, 2017 07:10

Comments resolved.

@floreks floreks merged commit d8f28e8 into kubernetes:master Jun 16, 2017
@floreks floreks mentioned this pull request Jun 16, 2017
@lenartj lenartj deleted the attach-proto branch June 16, 2017 18:49
@moon03432
Copy link

moon03432 commented Jul 7, 2017

@lenartj Thanks for your great feature! I deployed it in k8s 1.6.1 with
kubectl apply -f https://lenart.io/kubernetes-dashboard-head-attach-proto.yaml

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?

exec-command-in-dashboard-head

kube-dashboard-error

Dashboard crashes when an oci runtime error occurs. Maybe this error should be handled.

@floreks
Copy link
Member

floreks commented Jul 7, 2017

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

@cheld
Copy link
Contributor

cheld commented Jul 7, 2017

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?

@lenartj
Copy link
Contributor Author

lenartj commented Jul 7, 2017

Sorry I only noticed this comment now. I will be able to take a look on Wednesday, I am busy until that :/

@moon03432
Copy link

I'm just using docker 1.12.6, nothing special

@Kaijun
Copy link
Contributor

Kaijun commented Aug 3, 2017

Hmm, it seems that the dashboard pod will exit with error if you're executing a non-existing command.

@floreks
Copy link
Member

floreks commented Aug 3, 2017

This is a different issue related to session handling. We are working on a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA.
Projects
None yet
Development

Successfully merging this pull request may close these issues.