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

Client enhancements #2

Merged
merged 10 commits into from
Nov 17, 2016
Merged

Client enhancements #2

merged 10 commits into from
Nov 17, 2016

Conversation

mbohlool
Copy link
Contributor

@mbohlool mbohlool commented Nov 2, 2016

fixes #4

@sebgoa
Copy link
Contributor

sebgoa commented Nov 10, 2016

there is a requirement on pyyaml to be added, for the k8sutil to work on python 2.7

@mbohlool
Copy link
Contributor Author

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.

@mbohlool mbohlool changed the title Add SSL and kube config support and examples. Remove unnecessary tests Client first real enhancements Nov 11, 2016
@mbohlool mbohlool changed the title Client first real enhancements Client enhancements Nov 11, 2016
@mbohlool
Copy link
Contributor Author

@mml @smarterclayton @caesarxuchao Friendly ping.

@caesarxuchao
Copy link

First 3 commits lgtm.

@caesarxuchao
Copy link

The logic of the 4th commit makes sense. I'm not familiar with Python, so I can't give comments on the coding.

try:
# Divergence from upstream: ipaddress can't handle byte str
host_ip = _ip_address(hostname)
san = cert.get('subjectAltName', ())

Choose a reason for hiding this comment

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

What is ()?

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.

Copy link
Contributor Author

@mbohlool mbohlool Nov 16, 2016

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']))

Choose a reason for hiding this comment

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

What does SimpleNamespace do?

Copy link
Contributor Author

@mbohlool mbohlool Nov 16, 2016

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.

Copy link

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

http://norvig.com/python-iaq.html

Copy link
Contributor Author

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.

@sebgoa
Copy link
Contributor

sebgoa commented Nov 16, 2016

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.

Copy link

@marun marun left a 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']))
Copy link

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

http://norvig.com/python-iaq.html


def _find_return_type(func):
for line in pydoc.getdoc(func).splitlines():
if line.startswith(":return:"):
Copy link

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.

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.

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"):
Copy link

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

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.

# 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]
Copy link

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.

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.

else:
prev = ""
for l in lines:
if l:
Copy link

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

Copy link
Contributor Author

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):
Copy link

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.

Copy link
Contributor Author

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.

Copy link

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

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.

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.

return name


def _find_object_with_name(o, name, o_name):
Copy link

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.

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

ditto

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.

pass
return prev_match_hostname(cert, hostname)

def fix_ssl_hosts_with_ipaddress():
Copy link

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.

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

Copy link
Contributor Author

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.

@mbohlool
Copy link
Contributor Author

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

@mbohlool
Copy link
Contributor Author

mbohlool commented Nov 16, 2016

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

TYPE_LIST_SUFFIX = "List"


class SimpleNamespace:

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

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

Copy link

@marun marun left a 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
Copy link

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.

Copy link
Contributor Author

@mbohlool mbohlool Nov 16, 2016

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

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.

Copy link
Contributor Author

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

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

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

os.remove(f)


def _create_temp_file_with_content(content):
Copy link

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

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.

Copy link
Contributor Author

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:
Copy link

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.

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

for l in iter_resp_lines(resp):
j = json.loads(l)
j['raw_object'] = j['object']
if return_type:
Copy link

@marun marun Nov 16, 2016

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

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.

@mbohlool
Copy link
Contributor Author

I've applied/replied to all comments and will merge this.

Thanks @marun, @sebgoa, @DirectXMan12, @caesarxuchao for review.

@mbohlool mbohlool added kind/feature Categorizes issue or PR as related to a new feature. lgtm "Looks good to me", indicates that a PR is ready to be merged. labels Nov 17, 2016
@mbohlool mbohlool added this to the v1.0 milestone Nov 17, 2016
@mbohlool mbohlool merged commit 62dc349 into master Nov 17, 2016
yliaog pushed a commit to yliaog/client-python that referenced this pull request Jan 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature Categorizes issue or PR as related to a new feature. lgtm "Looks good to me", indicates that a PR is ready to be merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

cannot create resources
7 participants