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

Kube config loader improvement #50

Merged
merged 3 commits into from
Dec 2, 2016
Merged

Kube config loader improvement #50

merged 3 commits into from
Dec 2, 2016

Conversation

mbohlool
Copy link
Contributor

Current implementation of kube config loader, loads the default contexts. In this PR, it changed to load any context or list contexts. Also the code is refactored for better testability. kube config loader tests also added.

fixes #46

@mbohlool mbohlool added this to the v1.0 milestone Nov 30, 2016
@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Nov 30, 2016
if not contexts:
print("Cannot find any context in kube-config file.")
return
for i in range(len(contexts)):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't you deal with the context names directly ? something like

context_names =[]
for context in contexts:
    context_names.append(context['name'])
context_name = input('enter context name')
if context_name in context_names:
   config.load_kube_config(os.environ["HOME"] + '/.kube/config', context_name)
else:
    return "wrong name, try again"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason is a deleted part of the example that show hostname of the selected context. But even for that, it can be done easier. I will fix the example tomorrow. Thanks.

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 enumerating the list items rather than indexing into the list:

for i, context in enumerate(contexts):
 ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sebgoa Context names can be verbose, I prefer inputing the index number of the context for a simpler example. But I've applied most part of your commend (and @marun's).

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 nits and suggestions, but a few minor issues need addressing.

if not contexts:
print("Cannot find any context in kube-config file.")
return
for i in range(len(contexts)):
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 enumerating the list items rather than indexing into the list:

for i, context in enumerate(contexts):
 ...

v1 = client.CoreV1Api()
print("Listing pods with their IPs:")
ret = v1.list_pod_for_all_namespaces(watch=False)
for i in ret.items:
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 avoiding the use of single-character variable names for non-index variables.

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.

# limitations under the License.


class ConfigException(Exception):
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) Why is this necessary? Python exceptions have similar behavior by default. Unless there's a specific case that requires more rigor, I'd suggest YAGNI (you ain't gonna need it).

>>> str(Exception('foo'))
'foo'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you talking about the whole class or the message part? I agree about the message part, but the whole ConfigException class let use separate config related exception from other type of exceptions. It gives caller more control on what to do if it failed. I will, however, get rid of the message. I've added it because python3 does not have a message field (at least directly accessible message field) but I can just use str(*).

Copy link

Choose a reason for hiding this comment

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

The class is fine, just the message part.

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.

_TEST_ENVIRON = {_SERVICE_HOST_ENV_NAME: _TEST_HOST,
_SERVICE_PORT_ENV_NAME: _TEST_PORT}
_SERVICE_PORT_ENV_NAME: _TEST_PORT,
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) Why is it necessary to be able to vary the name of the env var between runtime and test? I get varying the filenames since that enables testing isolation, but the env dict can be isolated without varying the env var names.

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.

@@ -57,7 +60,7 @@ def get_test_loader(
if not token_filename:
token_filename = self._create_file_with_temp_content(_TEST_TOKEN)
if not cert_filename:
cert_filename = self._create_file_with_temp_content()
cert_filename = self._create_file_with_temp_content(_TEST_CERT)
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 defaulting content to _TEST_CERT instead of "" since that's the more common case.

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 agree that numbers here suggest _TEST_CERT is more popular (3 against 2) but having a CERT being default for a function that does not have anything to do with certificates does not feel right. As the usage numbers of these two values are close I would prefer having empty string as default value (I feel right to have things like empty string as default value unless the default value means something to the behaviour of the function).

return name


class TestFileOrData(TestRoot):
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 testing _create_temp_file_with_content directly rather than validating only through FileOrData.

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 test_create_temp_file_with_content. I've also simplified create_temp_file_with_content method and moved base64 out of it.

message_part in e.message, "'%s' should be in '%s'" %
(message_part, e.message))

def test_invalid_key(self):
Copy link

@marun marun Nov 30, 2016

Choose a reason for hiding this comment

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

The name of this test doesn't do a good job of communicating its intent. Consider renaming to something more descriptive.

Also, unit testing best practice is to make as few assertions as possible in a given test, and ideally each code path would get its own test.

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.

def test_invalid_key(self):
node = ConfigNode("test_obj", self.test_obj)
self.expect_exception(lambda: node['non-existance-key'],
"Expect key non-existance-key in test_obj")
Copy link

Choose a reason for hiding this comment

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

Duplicating error messages in testing makes for brittle tests. If testing is comprehensive, it should be enough to isolate the failure condition:

def setUp(self):
  self.node = ConfigNode("test_obj", self.test_obj)

def test_get_with_name_fails_if_value_not_a_list(self):
  self.node._value = None
  with self.assertRaises(ConfigException):
    self.node.get_with_name('')

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 agree generally, but here the purpose of ConfigNode is to have meaningful error messages. Specially full path of the config node that has the problem. maybe I can only check for the path itself? like in the above one, only "test_obj" or later ones for "test_obj/key3" etc.

Copy link

Choose a reason for hiding this comment

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

Given that the node name appears in most of the exceptions, I'm not sure what checking for it would buy you over the exception itself. If you want to validate anything beyond the fact that an exception is raised, consider using setting error codes on the exception that can be referred to via constants at both run and test time (e.g. IOException).

self.assertEqual("test_obj_with_names[name=test_name3]",
node.get_with_name("test_name3").name)

def expect_exception(self, func, message_part):
Copy link

Choose a reason for hiding this comment

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

There are better ways to do this:

def test_foo(self):
  with self.assertRaises(MyException) as context:
    foo()
  self.assertIn('bar', context.exception)

Reference: https://docs.python.org/2/library/unittest.html#assert-methods

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 "Config(%s\n)" % rep


class TestKubeConfigLoader(TestRoot):
Copy link

@marun marun Nov 30, 2016

Choose a reason for hiding this comment

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

(No action required) Validating entirely through load_and_set makes it hard to reason about which code paths are validated for a given test. That might be acceptable for an integration or e2e test, but for unit testing, consider ensuring comprehensive coverage of the lowest-level functions and only then consider the higher level testing required.

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 will add more unit tests for lower level functions, but keep these ones as it test the overall behaviour.

@mbohlool
Copy link
Contributor Author

Addressed all of your comments. Please take another look and apply LGTM label if it looks good. Thanks.

Copy link
Contributor

@SEJeff SEJeff left a comment

Choose a reason for hiding this comment

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

Just some suggestions. Figured I should read some PRs to start understanding this code. Overall great stuff



def main():
config_file = os.environ["HOME"] + '/.kube/config'
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be the "new" syntax like in #51 please?

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


# Configs can be set in Configuration class directly or using helper
# utility
config.load_kube_config(os.environ["HOME"] + '/.kube/config', context_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above. Perhaps make a constant at the top? KUBE_CONFIG_FILE or something?

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 can just reuse config_file variable. Done.

SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
SERVICE_PORT_ENV_NAME = "KUBERNETES_SERVICE_PORT"
SERVICE_TOKEN_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/token"
SERVICE_CERT_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is hardcoded on Linux containers, but is that the case on windows containers? I'm totally a Linux full time guy (typing from a Fedora 25 laptop), but these things seem relevant if k8s is going to run on windows, the client likely might as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. This is also hard-coded in k8s code. I would suggest you start a conversation on this on the main repo and if we changed the main repo, we can update this one too.

for f in _temp_files:
os.remove(f)
for temp_file in _temp_files.values():
os.remove(temp_file)
Copy link
Contributor

@SEJeff SEJeff Dec 1, 2016

Choose a reason for hiding this comment

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

Spurious change, or just OCD? 😄

This is functionally 100% the same sans a better for loop variable name. The values() isn't necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_temp_files was array before and dict now, so I cannot do it without values() (it will give me the keys in case of dict).

Copy link
Contributor

Choose a reason for hiding this comment

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

gah, I must have been smoking crack, you're entirely right here.

return self._current_context.value


class ConfigNode:
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is going to be used in python 2.7, can this be a new style class please?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!

@mbohlool
Copy link
Contributor Author

mbohlool commented Dec 1, 2016

Address @SEJeff comments too. PTAL.

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.

A minor note regarding unnecessary public scoping for use in testing.

As an aside, I find it really difficult to re-review non-trivial PR's such as this one without a patch history. I would appreciate either fixup patches so I can see what changes (and then those fixups would be squashed before merge) or a tool like reviewable that keeps a history of all commit versions for the pr.


def _join_host_port(host, port):

def join_host_port(host, port):
Copy link

Choose a reason for hiding this comment

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

I'm assuming you removed the _ prefix because it is now imported by the test module. That isn't necessary - a leading underscore indicating 'private' is a good convention but one that unit tests do not have to respect. Unless you want to have to support this function for users outside this module, I suggest reverting the name change.

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 agree that tests should not respect that convention, but to be able to use this method outside of this module, one should import it in init. Because I saw that gateway exists and it is still private to module, I did not see any problem make it package access. I however have no strong opinion about it, so I will just revert this one, but we can discuss if init gateway is enough for being private inside a package or we need to always follow _ convention.

@@ -22,151 +22,243 @@
from kubernetes.client import configuration
from oauth2client.client import GoogleCredentials

_temp_files = []
from .incluster_config import ConfigException
Copy link

Choose a reason for hiding this comment

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

Why isn't this imported from .config_exception? Consider avoiding relying on another module's imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed. (ConfigException was in incluster_config before, I moved it but apparently in python you can steal other modules import, I don't understand the benefit of that :)



def _create_temp_file_with_content(content):
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.

Same comment regarding private scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ditto

@SEJeff
Copy link
Contributor

SEJeff commented Dec 1, 2016

lgtm!

@mbohlool
Copy link
Contributor Author

mbohlool commented Dec 1, 2016

@marun PTAL @SEJeff Thanks.

@marun
Copy link

marun commented Dec 1, 2016

LGTM

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.

Support kube-config change context
5 participants