-
Notifications
You must be signed in to change notification settings - Fork 3.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
Client enhancements #2
Conversation
099def5
to
6b15ae9
Compare
7440d2f
to
e73968e
Compare
there is a requirement on pyyaml to be added, for the k8sutil to work on python 2.7 |
Good catch sebgoa. I will add it. I will update this PR with much better commits soon. Support for watch streaming response is among them. |
ca25bfe
to
9094775
Compare
@mml @smarterclayton @caesarxuchao Friendly ping. |
First 3 commits lgtm. |
1a2ac90
to
52d481f
Compare
The logic of the 4th commit makes sense. I'm not familiar with Python, so I can't give comments on the coding. |
c171fa4
to
51512ca
Compare
try: | ||
# Divergence from upstream: ipaddress can't handle byte str | ||
host_ip = _ip_address(hostname) | ||
san = cert.get('subjectAltName', ()) |
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.
What is ()
?
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 my understanding correct: this commit is fixing match_hostname function of python version < v3, so that if the hostname is an IP address, it can still match.
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 default value, it means an empty tuple will return if cert does not have 'subjectAltName'.
>>> x=()
>>> type(x)
<type 'tuple'>
>>> len(x)
0
>>>
Your understanding is correct.
j = json.loads(l) | ||
j['raw_object'] = j['object'] | ||
if return_type: | ||
obj = SimpleNamespace(data=json.dumps(j['raw_object'])) |
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.
What does SimpleNamespace do?
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.
ref: https://docs.python.org/3.5/library/types.html#types.SimpleNamespace
It is something that available in python3 not python2. it lets you have types with dynamic attributes. api_client.deserialize expect a type that has 'data' attribute.
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.
Peter Norvig popularized this before py3 on his IAQ (as 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.
Thanks for sharing Peter Norvig's IAQ. I enjoyed reading it. Though I like the name struct, I would keep SimpleNamespace for folks familiar with concept in py3.
Honestly this PR is so big with auto-generated code, you should just merge it and move on in master. I think it is more important to setup build bots, pip package, proper README and a bit of doc. Right now what is in master is broken. |
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.
Mostly suggestions, but I have concerns about the proposed monkeypatch.
j = json.loads(l) | ||
j['raw_object'] = j['object'] | ||
if return_type: | ||
obj = SimpleNamespace(data=json.dumps(j['raw_object'])) |
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.
Peter Norvig popularized this before py3 on his IAQ (as Struct
):
|
||
def _find_return_type(func): | ||
for line in pydoc.getdoc(func).splitlines(): | ||
if line.startswith(":return:"): |
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.
Consider defining :return:
as a constant and using a name that describes its purpose.
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.
return_type = _find_return_type(func) | ||
# Hacky assumption that watching `func` return TypeList in non-watch mode and an event with | ||
# object of type in Type in watch mode. | ||
if return_type.endswith("List"): |
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.
Consider defining "List" as a constant...
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.
# Hacky assumption that watching `func` return TypeList in non-watch mode and an event with | ||
# object of type in Type in watch mode. | ||
if return_type.endswith("List"): | ||
return_type = return_type[:len(return_type)-4] |
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.
...and using len(constant) instead of the magic number 4.
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.
else: | ||
prev = "" | ||
for l in lines: | ||
if l: |
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.
Consider avoiding unnecessary indentation:
if not l:
continue
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.
Not relevant anymore because of previous refactoring.
os.remove(f) | ||
|
||
|
||
def _create_temp_file_with_content(content): |
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.
Rather than managing many created tmp files, consider tracking a single directory created using mkdtemp
and call mkstemp
against that path.
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 that is be better? I mean I am just letting os do what is best. There is no requirement for these files to be in the same folder.
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.
(No action required)
I use 'consider' to suggest a course of action, not demand a change. I think it's cleaner to isolate multiple files created by an application in a directory, but if you don't agree there's no need to change.
return _create_temp_file_with_content(o[data_key_name]) | ||
if file_key_name in o: | ||
return o[file_key_name] | ||
return None |
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.
Consider removing this line, since python returns None by default.
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.
return name | ||
|
||
|
||
def _find_object_with_name(o, name, o_name): |
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.
Consider avoiding the use of single-character variables for anything but loop indexes. obj
might be more appropriate here. Also, what is the import of name
vs o_name
? It's non-obvious to me, and likely to be non-obvious to future maintainers.
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 whole function was obsolete. sorry for that. removed. (name was the value of attribute 'name' of the object, and o_name was a debug friendly name of the object such as 'user' or 'cluster'. the logic moved to _node.get_with_name)
with open(o[file_key_name], 'r') as f: | ||
data = f.read() | ||
return data | ||
return None |
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.
ditto
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.
pass | ||
return prev_match_hostname(cert, hostname) | ||
|
||
def fix_ssl_hosts_with_ipaddress(): |
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 this really the best way to solve this problem? Monkeypatches are risky even when the patch is in code intended to be consumed as an application (and therefore packaged with a known good version of the targeted dependency). Patching in a library is generally a bad idea because there is no guarantee that the consumer will have a compatible patch target, and bugs in the patch may prove difficult to track down.
If this is really the only option, consider adding an explicit warning that monkeypatching is being performed to the README to aid in troubleshooting.
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 the ugliness in what I am doing here and I completely agree with you that is not a clean fix. I spend some time on this. basically py2.7 does not support ipaddress hosts and the fix has been added here: https://bugs.python.org/issue23239 for python 3.x. I am open to suggestions here and I will keep looking for a better solution. For now I will go with your suggestion of adding a warning to README file.
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.
Look like this have been fix in newer versions of urllib3. I will confirm that and remove this hack all-together in that case.
@sebgoa I would like to have at least one person with python background to sign off on this before merging. The auto-generated code can be ignored and the rest of the code should not be hard to review. |
@marun Thanks for the review. I've applied or replied to your comments. Notably I've removed ssl hack and changed dependency to a newer version of urllib3 that fixes the ip address hosts. Please take another look. |
cada429
to
16ce003
Compare
TYPE_LIST_SUFFIX = "List" | ||
|
||
|
||
class SimpleNamespace: |
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 can probably replace this with a named tuple (see collections.namedtuple
), if you know what fields you need (and it looks like there's only one use below, so you probably can).
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 this will work with almost any class except object. even something like
class MyObject(object):
pass
resp = MyObject()
resp.data='...'
...
d=resp.data
will work. SimpleNamespace is the simplest solution here as it will accept the attribute dict as the only parameter without any other complication. collections.namedtuple can also work with passing more parameters at constructor and adding some confusion why we used a namedtuple. My understanding is SimpleNamespace is the standard way to do what I want to do in Py3 so I am still in favor of using 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.
Good work cleaning things up and removing the monkeypatch!
I think the testing could be better, but I'm really picky. I leave it to your discretion as to whether you want to follow through with my suggestions.
Otherwise, I agree with @sebgoa that this should merge and automated testing should be the next step. Reviewing will only get us so far when generated code is such a substantial component.
|
||
import unittest | ||
from .watch import Watch | ||
from mock import Mock |
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.
(No action required)
Regarding my now-lost comment regarding the wisdom of mocking (thanks github), I think that having to use mocks to test code that one has written is often an indication of poorly-factored code. Mocks have their uses when one has to validate behavior against external libraries, but most of the code here is under your control and could be structured to be tested largely without mocks.
Here's a good overview of the philosophy I'm espousing: Stop Mocking, Start Testing
I've suggested some additional refactors of watch.py that would make for simpler testing. Ideally all the lower-level functionality could be tested in isolation, and then a single high-level test (similar in scope to one of the tests here, and using mocks) could ensure that the pieces tie together as intended.
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.
Thanks for the video. It was useful. I agree that complicated Mocks will make things harder to follow and keeping test/mock in sync with code would be harder. My point here is the Mock is a simple request mock and does not have much logic. Maybe if I refactor the test itself, it would be more clear. Nevertheless, you refactoring suggestions are good and I am going to apply them as much as possible.
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import unittest |
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.
(No action required)
Consider ordering imports alphabetically. Not sure if there is a policy on this for kube, its just a general convention.
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.
Added a semi-automatic script to do these things.
self._stop = True | ||
|
||
def stream(self, func, *args, **kwargs): | ||
"""Watch an API resource and stream the result back as a generators. |
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.
nit: as a generators -> via a generator
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
os.remove(f) | ||
|
||
|
||
def _create_temp_file_with_content(content): |
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.
(No action required)
I use 'consider' to suggest a course of action, not demand a change. I think it's cleaner to isolate multiple files created by an application in a directory, but if you don't agree there's no need to change.
@@ -0,0 +1,148 @@ | |||
# Copyright 2016 The Kubernetes Authors. |
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.
Consider writing tests for this module.
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 do.
watch.stop() | ||
""" | ||
|
||
if self._return_type: |
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.
(No action required)
Lines 90-95 could be factored out into a method (e.g. get_return_type) and unit tested independently.
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
for l in iter_resp_lines(resp): | ||
j = json.loads(l) | ||
j['raw_object'] = j['object'] | ||
if return_type: |
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.
(No action required)
Consider factoring out the marshaling so it can be isolated for testing, e.g.
def marshall_obj(data, api_client, return_type):
j = json.loads(data)
j['raw_object'] = j['object']
if return_type:
...
return j
def wait(...):
...
try:
for l in iter_resp_lines(resp):
obj = marshall_obj(l, api_client, return_type)
yield obj
...
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 applied/replied to all comments and will merge this. Thanks @marun, @sebgoa, @DirectXMan12, @caesarxuchao for review. |
Remove loop symlinks
Adding SSL support (a fix for python 2.7 to accept ipaddress hosts)fixes #4