Skip to content
This repository has been archived by the owner on Jul 9, 2019. It is now read-only.

Commit

Permalink
Merge pull request #15 from Ultimaker/k8s
Browse files Browse the repository at this point in the history
Fixing issue with default fields
  • Loading branch information
ChrisTerBeke authored Jun 13, 2018
2 parents bee9e28 + 30b6151 commit fb34145
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 23 deletions.
32 changes: 16 additions & 16 deletions mongoOperator/models/BaseModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing import Dict

from mongoOperator.models.fields import Field, pascal_to_lowercase
from mongoOperator.models.fields import Field, lowercase_to_pascal


class BaseModel:
Expand All @@ -22,13 +22,13 @@ def __init__(self, **kwargs):
"""
self.fields = dict(inspect.getmembers(type(self), lambda f: isinstance(f, Field))) # type: Dict[str, Field]

for field_name, value in kwargs.items():
for field_name, field in self.fields.items():
# K8s API returns pascal cased strings, but we use lower-cased strings with underscores instead.
field_lower = pascal_to_lowercase(field_name)
if field_lower in self.fields:
setattr(self, field_lower, self.fields[field_lower].parse(value))
else:
logging.warning("The model %s does not have the %s field!", type(self), field_lower)
# we accept both in our models.
value = kwargs.get(field_name, kwargs.get(lowercase_to_pascal(field_name)))
if value is not None:
value = field.parse(value)
setattr(self, field_name, value)

def validate(self) -> None:
"""
Expand All @@ -40,17 +40,15 @@ def validate(self) -> None:
except (ValueError, AttributeError) as err:
raise ValueError("Error for field {}: {}".format(repr(name), err))

def to_dict(self) -> Dict[str, any]:
def to_dict(self, skip_validation: bool = False) -> Dict[str, any]:
"""
Returns a dictionary with the data of each field.
:param skip_validation: Whether the validation should be skipped.
:return: A dict with the attribute name as key and the attribute value.
"""
self.validate()
result = {}
for name, field in self.fields.items():
if self[name] is not None:
result[name] = field.to_dict(self[name])
return result
if not skip_validation:
self.validate()
return {name: field.to_dict(self[name], skip_validation=skip_validation) for name, field in self.fields.items()}

def __eq__(self, other: any) -> bool:
"""
Expand All @@ -75,5 +73,7 @@ def __repr__(self) -> str:
Shows the string-representation of this object.
:return: The object as string.
"""
return "{}({})".format(self.__class__.__name__,
", ".join('{}={}'.format(attr, value) for attr, value in self.to_dict().items()))
return "{}({})".format(
self.__class__.__name__,
", ".join('{}={}'.format(attr, value) for attr, value in self.to_dict(skip_validation=True).items())
)
34 changes: 27 additions & 7 deletions mongoOperator/models/fields.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Dict, Type
from typing import Dict, Type, Optional

import re

Expand All @@ -17,6 +17,17 @@ def pascal_to_lowercase(value: str) -> str:
return re.sub(r"([a-z0-9]+)([A-Z])", r"\1_\2", value).lower()


def lowercase_to_pascal(value: str) -> str:
"""
Converts a string from lower-case underscore separated strings into pascal case.
e.g. pascal_case => pascalCase.
Note: K8s API returns pascal cased strings, but in Python we use lower-cased strings with underscores instead.
:param value: The string to be converted.
:return: The converted string.
"""
return re.sub(r"([a-z0-9]+)_([a-z])", lambda m: m.group(1) + m.group(2).upper(), value)


class Field:
"""
Base field that can be used in the models. This field does no validation whatsoever.
Expand All @@ -42,13 +53,15 @@ def parse(self, value: any) -> any:
self.validate(value)
return value

def to_dict(self, value) -> any:
def to_dict(self, value, skip_validation: bool = False) -> any:
"""
Returns a the value of this field as it should be set in the model dictionaries.
:param value: The value to be converted.
:param skip_validation: Whether the validation should be skipped.
:return: The value of this field.
"""
self.validate(value)
if not skip_validation:
self.validate(value)
return value


Expand All @@ -74,12 +87,19 @@ def __init__(self, field_type: Type, required: bool):

def parse(self, value: Dict[str, any]):
if isinstance(value, dict):
# K8s API returns pascal cased strings, but we use lower-cased strings with underscores instead.
values = {pascal_to_lowercase(field_name): field_value for field_name, field_value in value.items()}
value = self.field_type(**values)
try:
# K8s API returns pascal cased strings, but we use lower-cased strings with underscores instead.
values = {pascal_to_lowercase(field_name): field_value for field_name, field_value in value.items()}
value = self.field_type(**values)
except TypeError as err:
raise ValueError("Invalid values passed to {} field: {}. Received {}."
.format(self.field_type.__name__, err, value))
return super().parse(value)

def to_dict(self, value) -> Dict[str, any]:
def to_dict(self, value, skip_validation: bool = False) -> Optional[Dict[str, any]]:
value = super().to_dict(value, skip_validation)
if value is None:
return None
return {k: v for k, v in value.to_dict().items() if v is not None}


Expand Down
14 changes: 14 additions & 0 deletions tests/models/TestV1MongoClusterConfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from unittest import TestCase
from unittest.mock import patch

from kubernetes.client import V1SecretKeySelector

Expand All @@ -26,6 +27,19 @@ def test_example(self):
self.cluster_dict["spec"]["backups"]["gcs"]["service_account"].pop("secretKeyRef")
self.assertEquals(self.cluster_dict, self.cluster_object.to_dict())

def test_wrong_values_kubernetes_field(self):
self.cluster_dict["metadata"] = {"invalid": "value"}
with self.assertRaises(ValueError) as context:
V1MongoClusterConfiguration(**self.cluster_dict)
self.assertEqual("Invalid values passed to V1ObjectMeta field: __init__() got an unexpected keyword argument "
"'invalid'. Received {'invalid': 'value'}.", str(context.exception))

def test_embedded_field_none(self):
del self.cluster_dict["metadata"]
self.cluster_object.metadata = None
self.assertEqual(self.cluster_object.to_dict(skip_validation=True),
V1MongoClusterConfiguration(**self.cluster_dict).to_dict(skip_validation=True))

def test_non_required_fields(self):
cluster_dict = getExampleClusterDefinition(replicas=5)
cluster_object = V1MongoClusterConfiguration(**cluster_dict)
Expand Down
13 changes: 13 additions & 0 deletions tests/services/TestKubernetesService.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,19 @@ def test_createStatefulSet(self, client_mock):
self.assertEqual(expected_calls, client_mock.mock_calls)
self.assertEqual(client_mock.AppsV1beta1Api().create_namespaced_stateful_set.return_value, result)

def test_createStatefulSet_no_optional_fields(self, client_mock):
service = KubernetesService()
client_mock.reset_mock()
del self.cluster_dict["spec"]["mongodb"]["cpu_limit"]
del self.cluster_dict["spec"]["mongodb"]["memory_limit"]
self.cluster_object = V1MongoClusterConfiguration(**self.cluster_dict)

expected_calls = [call.AppsV1beta1Api().create_namespaced_stateful_set(self.namespace, self.stateful_set)]

result = service.createStatefulSet(self.cluster_object)
self.assertEqual(expected_calls, client_mock.mock_calls)
self.assertEqual(client_mock.AppsV1beta1Api().create_namespaced_stateful_set.return_value, result)

def test_updateStatefulSet(self, client_mock):
service = KubernetesService()
client_mock.reset_mock()
Expand Down

0 comments on commit fb34145

Please sign in to comment.