-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Improvements on ws_client #125
Conversation
f26ae30
to
df4c73b
Compare
Codecov Report
@@ Coverage Diff @@
## master #125 +/- ##
=======================================
Coverage 94.46% 94.46%
=======================================
Files 9 9
Lines 668 668
=======================================
Hits 631 631
Misses 37 37 Continue to review full report at Codecov.
|
kubernetes/client/ws_client.py
Outdated
|
||
# TODO: This method does not seem to work. | ||
def write_stdin(self, data): | ||
self.sock.send(data) |
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.
Is the connection still open to even send the data? How does the underlining api url call look like when you try to tack on more to exec
commands to an already successful call?
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.
The connection stays open until explicitly closed by either side. My example code does not close the connection before sending data.
examples/exec.py
Outdated
print("STDERR: %s" % resp.read_stderr()) | ||
|
||
exit(1) | ||
# This part does not work yet. resp.write_stdin does not work. |
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.
Could it be that you need to set the websocket to lingering, so that the connection stays open on both ends till the action you are trying to execute completes and forwards back the result?
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.
In the example bellow, I only run sh without closing the connection. _preload_content is False so the call will stays open. Then on line 94 inside the loop I send commands one by one. This example code will not end by itself because the connection stays open.
@mrmcmuffinz @dims this is ready for review. |
If you don't mind I can review it tomorrow, I'm on the east coast and it's close to midnight here. I want to get some sleep. |
@mrmcmuffinz no rush. thanks. |
examples/exec.py
Outdated
from kubernetes.client.rest import ApiException | ||
|
||
config.load_kube_config( | ||
context="gke_cloud-kubernetes-dev_us-central1-f_mehdy-cluster") |
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.
have a more generic name here?
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.
@mbohlool : looks good, i haven't tried it yet. Though that reminds me that we should add a e2e test especially for the new code path where we pull data on demand. |
@dims. Will add an e2e test. |
examples/exec.py
Outdated
command=exec_command, | ||
stderr=True, stdin=False, | ||
stdout=True, tty=False) | ||
print "Response: " + resp |
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 you think it would help to demonstrate here how to access the stdout and stderr?
What happens when you print to stderr
but when you call exec
function with stderr=False
and vice versa with stdout
?
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.
There are many cases that we can cover. We can add more examples later (and feel free to add more).
exec_command = ['/bin/sh'] | ||
resp = api.connect_get_namespaced_pod_exec(name, 'default', | ||
command=exec_command, | ||
stderr=True, stdin=True, |
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 mentioned in the comments that stdin
is not working, can we get rid of that argument here?
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.
- stdin is part of auto-generated arguments that we never touch.
- stdin is working now :)
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.
Awesome!
@@ -23,22 +25,17 @@ | |||
|
|||
class WSClient: | |||
def __init__(self, configuration, url, headers): |
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 we add some documentation to this?
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 will add some documentation for methods in this class. init specifically does not need docs in my opinion as this class is only created inside this file.
kubernetes/client/ws_client.py
Outdated
self._connected = False | ||
self._stdout = "" | ||
self._stderr = "" | ||
self._all = "" | ||
|
||
# We just need to pass the Authorization, ignore all the other | ||
# http headers we get from the generated code | ||
if 'Authorization' in headers: |
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 to be concerned that headers is None or that there is no Authorization
in 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.
if check to see if there is authorization in it. if there is none, we don't need to pass any header. Fixed the case that headers is None.
kubernetes/client/ws_client.py
Outdated
self.update() | ||
return self._stderr | ||
|
||
def read_stderr(self): |
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.
Where was this in the example?
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.
end of the example, I've used this method.
import collections | ||
import websocket | ||
from websocket import WebSocket, ABNF, enableTrace |
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 a suggestion however can we organize these, import
first then from
?
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.
we have pep8 and sort_import running automatically and checking these. We cannot organize them the way we want unless we want to remove those checks.
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 see.
kubernetes/client/ws_client.py
Outdated
def is_open(self): | ||
return self._connected | ||
|
||
# TODO: This method does not seem to work. |
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 this does not work, do it make sense to keep it in the code or remove 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.
old comment. removed.
if op_code == ABNF.OPCODE_CLOSE: | ||
self._connected = False | ||
return | ||
elif op_code == ABNF.OPCODE_BINARY or op_code == ABNF.OPCODE_TEXT: |
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.
Is there any other op_code
that needs to be considered other than the ones you caught here and above?
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 don't think so.
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 don't know much about sockets however I see that there are more codes https://github.com/websocket-client/websocket-client/blob/master/websocket/_abnf.py#L102 where I thought we may have to cover our bases but I would like to leave that up to someone else who is more experienced in the area.
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've modeled this from WebSocketApp here: https://github.com/websocket-client/websocket-client/blob/master/websocket/_app.py#L205. Ping and Pong are optional (and we can support it later). Cont can be ignored and it is only useful if you want to get notified on an incomplete frame. however, we may improve this later by doing something on "Cont" (like running update again).
kubernetes/client/ws_client.py
Outdated
|
||
|
||
WSResponse = collections.namedtuple('WSResponse', ['data']) | ||
|
||
|
||
def GET(configuration, url, query_params, _request_timeout, headers): | ||
def GET(configuration, url, query_params, _request_timeout, _preload_content, | ||
headers): |
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.
Documentation?
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.
GET is internal, but above methods need docs that will be added.
kubernetes/client/ws_client.py
Outdated
self._stdout += data | ||
|
||
def run_forever(self, timeout=None): | ||
if timeout: |
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 we get an explanation on what run_forever means and are there any gotchas(what are the assumptions you are making)?
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.
will add documentation.
@mrmcmuffinz, @dims addressed comments and also make the implementation more general to support any channel number (0,1,2 for std in,out,err). We may want to reuse this again in port forwarding api calls (they use websocket too). PTAL. |
fd956cd
to
c20d9a8
Compare
General comment, was your intention to expose the channel constants for the end user like myself or just for the purpose of the websocket client? |
@mrmcmuffinz Channel numbering is a kubernetes thing as far as I can tell and I think it is not bad to let user know about it. But the helper function like read_stdout can hide that if user don't want to care about them. The whole channels expose can be useful later for port forwarding api implementation. |
kubernetes/client/ws_client.py
Outdated
on_close=self.on_close, | ||
header=[header] if header else None) | ||
self.ws.on_open = self.on_open | ||
if headers and 'Authorization' in headers: |
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'm running into a small issue when running the exec.py example using header authentication.
Looks like is a small case condition; it appears that the header key is "authorization" instead of "Authorization" and then we pass an empty header to the WS call.
(kubernetes-local) PAOC02S79NJG8WP:examples sa$ python exec.py
{'Content-Type': 'application/json', 'authorization': 'Bearer CLSMM1PCEbKNOgy2fz5runETCrf-yzV1ThPXAiJ4aLw', 'Accept': '*/*', 'User-Agent': 'Swagger-Codegen/1.0.0-snapshot/python'}
Traceback (most recent call last):
File "exec.py", line 61, in <module>
stdout=True, tty=False)
File "/Users/sagui10/git/kubernetes/client-python/kubernetes/client/apis/core_v1_api.py", line 907, in connect_get_namespaced_pod_exec
(data) = self.connect_get_namespaced_pod_exec_with_http_info(name, namespace, **kwargs)
File "/Users/sagui10/git/kubernetes/client-python/kubernetes/client/apis/core_v1_api.py", line 1012, in connect_get_namespaced_pod_exec_with_http_info
collection_formats=collection_formats)
File "/Users/sagui10/git/kubernetes/client-python/kubernetes/client/api_client.py", line 329, in call_api
_return_http_data_only, collection_formats, _preload_content, _request_timeout)
File "/Users/sagui10/git/kubernetes/client-python/kubernetes/client/api_client.py", line 153, in __call_api
_request_timeout=_request_timeout)
File "/Users/sagui10/git/kubernetes/client-python/kubernetes/client/api_client.py", line 355, in request
headers=headers)
File "/Users/sagui10/git/kubernetes/client-python/kubernetes/client/ws_client.py", line 239, in websocket_call
raise ApiException(status=0, reason=str(e))
kubernetes.client.rest.ApiException: (0)
Reason: Handshake status 403
I added this to debug and make it work:
# We just need to pass the Authorization, ignore all the other # http headers we get from the generated code if headers and 'Authorization' in headers: header.append("Authorization: %s" % headers['Authorization']) elif headers and 'authorization' in headers: header.append("Authorization: %s" % headers['authorization']) print headers
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.
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.
This should be always authorization with all lower case. I wonder why it worked for me before, I am investigating it. But I am agains making the check case insensitive as it will result in being inconsistent in different part of the code while we know this should be always lowecase.
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've fixed this to be lower case authorization to be consistent with the rest of the client, also created #128 to further investigate this.
…teract with websocket server and reach each channel separately
on_close=self.on_close, | ||
header=[header] if header else None) | ||
self.ws.on_open = self.on_open | ||
if headers and 'authorization' in headers: |
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.
Should we support both Authorization
and authorization
?
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 think we should be consistent. We used lowercase authorization
everywhere else in our client. Based on the discussion in #128, I think we should have capital Authorization
everywhere and case-insensitive checks. I will let another PR fixing #128 to do it consistently for whole client and keep this here to be consistent meanwhile.
One question inline, LGTM otherwise. thanks @mbohlool |
thanks @dims @mrmcmuffinz @chekolyn |
Hey @mbohlool to be clear I can not set in my pip requirements.txt |
Btw, thanks to all for being patient and following through on the comments. Awesome work! |
@chekolyn That would be fine. But I think it is better for pykube to add a dependency on client-python. It could simplify its config and transport layers too (by delegating them to client-python) and get any patch/update for free. |
@mrmcmuffinz you're welcome. v1.0.0b2 has a dependency problem that @dims is fixing. Was your question related to that? |
The ws_client should be able to return an object so the caller can interact with server. This will happen if _preload_content is not True. In that case, the client will return an object that can interact with stdin/stdout/stderr of the remote container.
fixes #58