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 #24 from Ultimaker/more-configuration-options
Browse files Browse the repository at this point in the history
More configuration options
  • Loading branch information
DanielSchiavini authored Nov 28, 2018
2 parents 8760b74 + b26d137 commit b12a80e
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 76 deletions.
10 changes: 0 additions & 10 deletions Settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import os


Expand All @@ -20,12 +19,3 @@ class Settings:

# Kubernetes config.
KUBERNETES_SERVICE_DEBUG = os.getenv("KUBERNETES_SERVICE_DEBUG") in STRING_TO_BOOL_DICT
KUBERNETES_NAMESPACE = os.getenv("KUBERNETES_NAMESPACE", "default")

# Mongo RS config.
MONGO_RS_POD_LABELS = os.getenv("MONGO_RS_POD_LABELS", "app=mongo")
MONGO_RS_SERVICE_NAME = os.getenv("KUBERNETES_MONGO_SERVICE_NAME")
MONGO_RS_SERVICE_PORT = os.getenv("MONGO_RS_SERVICE_PORT", 27017)
MONGO_RS_USERNAME = os.getenv("MONGO_RS_USERNAME", "")
MONGO_RS_PASSWORD = os.getenv("MONGO_RS_PASSWORD", "")
MONGO_RS_DATABASE = os.getenv("MONGO_RS_DATABASE", "local")
2 changes: 1 addition & 1 deletion mongoOperator/MongoOperator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, sleep_per_run: float = 5.0) -> None:
"""
self._sleep_per_run = sleep_per_run

def run_forever(self):
def run_forever(self) -> None:
"""
Runs the mongo operator forever (until a kill command is received).
"""
Expand Down
45 changes: 31 additions & 14 deletions mongoOperator/helpers/KubernetesResources.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,29 @@ class KubernetesResources:
"""
Helper class responsible for creating the Kubernetes model objects.
"""


# These are fixed values. They need to be these exact values for Mongo to work properly with the operator.
MONGO_IMAGE = "mongo:3.6.4"
MONGO_NAME = "mongodb"
MONGO_PORT = 27017
MONGO_COMMAND = "mongod --replSet {name} --bind_ip 0.0.0.0 --smallfiles --noprealloc"
MONGO_STORAGE_NAME = "mongo-storage"

STORAGE_SIZE = "30Gi"
STORAGE_MOUNT_PATH = "/data/db"

DEFAULT_CPU_LIMIT = "100m"
DEFAULT_MEMORY_LIMIT = "64Mi"
MONGO_COMMAND = "mongod \
--wiredTigerCacheSizeGB {cache_size} \
--replSet {name} \
--bind_ip 0.0.0.0 \
--smallfiles \
--noprealloc"

# These are default values and are overridable in the custom resource definition.
DEFAULT_STORAGE_NAME = "mongo-storage"
DEFAULT_STORAGE_SIZE = "30Gi"
DEFAULT_STORAGE_MOUNT_PATH = "/data/db"
DEFAULT_STORAGE_CLASS_NAME = None # when None is passed the value is simply ignored by Kubernetes

# Default resource allocation.
# See https://docs.mongodb.com/manual/administration/production-notes/#allocate-sufficient-ram-and-cpu.
DEFAULT_CPU_LIMIT = "1"
DEFAULT_MEMORY_LIMIT = "2Gi"
DEFAULT_CACHE_SIZE = "0.25"

@classmethod
def createSecret(cls, secret_name: str, namespace: str, secret_data: Dict[str, str],
Expand Down Expand Up @@ -100,8 +111,13 @@ def createStatefulSet(cls, cluster_object: V1MongoClusterConfiguration) -> clien
name = cluster_object.metadata.name
namespace = cluster_object.metadata.namespace
replicas = cluster_object.spec.mongodb.replicas
storage_name = cluster_object.spec.mongodb.storage_name or cls.DEFAULT_STORAGE_NAME
storage_size = cluster_object.spec.mongodb.storage_size or cls.DEFAULT_STORAGE_SIZE
storage_mount_path = cluster_object.spec.mongodb.storage_data_path or cls.DEFAULT_STORAGE_MOUNT_PATH
storage_class_name = cluster_object.spec.mongodb.storage_class_name or cls.DEFAULT_STORAGE_CLASS_NAME
cpu_limit = cluster_object.spec.mongodb.cpu_limit or cls.DEFAULT_CPU_LIMIT
memory_limit = cluster_object.spec.mongodb.memory_limit or cls.DEFAULT_MEMORY_LIMIT
wired_tiger_cache_size = cluster_object.spec.mongodb.wired_tiger_cache_size or cls.DEFAULT_CACHE_SIZE

# create container
mongo_container = client.V1Container(
Expand All @@ -115,17 +131,17 @@ def createStatefulSet(cls, cluster_object: V1MongoClusterConfiguration) -> clien
)
)
)],
command=cls.MONGO_COMMAND.format(name=name).split(),
command=cls.MONGO_COMMAND.format(name=name, cache_size=wired_tiger_cache_size).split(),
image=cls.MONGO_IMAGE,
ports=[client.V1ContainerPort(
name=cls.MONGO_NAME,
container_port=cls.MONGO_PORT,
protocol="TCP"
)],
volume_mounts=[client.V1VolumeMount(
name=cls.MONGO_STORAGE_NAME,
name=storage_name,
read_only=False,
mount_path=cls.STORAGE_MOUNT_PATH
mount_path=storage_mount_path
)],
resources=client.V1ResourceRequirements(
limits={"cpu": cpu_limit, "memory": memory_limit},
Expand All @@ -144,10 +160,11 @@ def createStatefulSet(cls, cluster_object: V1MongoClusterConfiguration) -> clien
spec = client.V1PodSpec(containers=[mongo_container]),
),
volume_claim_templates = [client.V1PersistentVolumeClaim(
metadata = client.V1ObjectMeta(name=cls.MONGO_STORAGE_NAME),
metadata = client.V1ObjectMeta(name=storage_name),
spec = client.V1PersistentVolumeClaimSpec(
access_modes = ["ReadWriteOnce"],
resources = client.V1ResourceRequirements(requests={"storage": cls.STORAGE_SIZE}),
resources = client.V1ResourceRequirements(requests={"storage": storage_size}),
storage_class_name = storage_class_name
),
)],
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker
# !/usr/bin/env python
# -*- coding: utf-8 -*-

from mongoOperator.models.BaseModel import BaseModel
from mongoOperator.models.V1MongoClusterConfigurationSpecBackupsGCS import V1MongoClusterConfigurationSpecBackupsGCS
from mongoOperator.models.fields import EmbeddedField, StringField
Expand All @@ -13,4 +12,3 @@ class V1MongoClusterConfigurationSpecBackups(BaseModel):
"""
gcs = EmbeddedField(V1MongoClusterConfigurationSpecBackupsGCS, required=True)
cron = StringField(required=False)

25 changes: 24 additions & 1 deletion mongoOperator/models/V1MongoClusterConfigurationSpecMongoDB.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker
# !/usr/bin/env python
# -*- coding: utf-8 -*-

from mongoOperator.models.BaseModel import BaseModel
from mongoOperator.models.fields import StringField, MongoReplicaCountField

Expand All @@ -10,8 +9,32 @@ class V1MongoClusterConfigurationSpecMongoDB(BaseModel):
"""
Model for the `spec.mongodb` field of the V1MongoClusterConfiguration.
"""

# The name of the deployment.
mongo_name = StringField(required=False)

# The name of the volumes that Kubernetes will create and mount. Defaults to mongo-storage.
storage_name = StringField(required=False)

# The size of the volumes that Kubernetes will create and mount. Defaults to 30Gi.
storage_size = StringField(required=False)

# The path on which the volumes should be mounted. Defaults to /data/db.
storage_data_path = StringField(required=False)

# The Kubernetes storage class to use in Kubernetes. Defaults to None.
storage_class_name = StringField(required=False)

# Kubernetes CPU limit of each Mongo container. Defaults to 1 (vCPU).
cpu_limit = StringField(required=False)

# Kubernetes memory limit of each Mongo container. Defaults to 2Gi.
memory_limit = StringField(required=False)

# Amount of Mongo container replicas. Defaults to 3.
replicas = MongoReplicaCountField(required=True)

# The wired tiger cache size. Defaults to 0.25.
# Should be half of the memory limit minus 1 GB.
# See https://docs.mongodb.com/manual/administration/production-notes/#allocate-sufficient-ram-and-cpu for details.
wired_tiger_cache_size = StringField(required=False)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PyYAML
kubernetes==5.0.0
kubernetes==8.0.0
PyMongo
croniter
google-cloud-storage
2 changes: 1 addition & 1 deletion tests/helpers/TestClusterChecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def test_streamEvents_bad_cluster(self, watch_mock):
stream_mock.assert_called_once_with(self.kubernetes_service.listMongoObjects,
_request_timeout=self.checker.STREAM_REQUEST_TIMEOUT)
self.assertTrue(watch_mock.return_value.stop)
self.assertEquals("100", watch_mock.return_value.resource_version)
self.assertEqual("100", watch_mock.return_value.resource_version)

@patch("mongoOperator.helpers.BaseResourceChecker.BaseResourceChecker.cleanResources")
@patch("mongoOperator.helpers.BaseResourceChecker.BaseResourceChecker.listResources")
Expand Down
13 changes: 9 additions & 4 deletions tests/models/TestV1MongoClusterConfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from unittest import TestCase
from unittest.mock import patch

from kubernetes.client import V1SecretKeySelector

Expand All @@ -25,7 +24,7 @@ def test_example(self):
self.cluster_dict["spec"]["backups"]["gcs"].pop("serviceAccount")
self.cluster_dict["spec"]["backups"]["gcs"]["service_account"]["secret_key_ref"] = \
self.cluster_dict["spec"]["backups"]["gcs"]["service_account"].pop("secretKeyRef")
self.assertEquals(self.cluster_dict, self.cluster_object.to_dict())
self.assertEqual(self.cluster_dict, self.cluster_object.to_dict())

def test_wrong_values_kubernetes_field(self):
self.cluster_dict["metadata"] = {"invalid": "value"}
Expand All @@ -46,13 +45,19 @@ def test_non_required_fields(self):
self.assertEqual(dict(replicas=5), cluster_dict["spec"]["mongodb"])
self.assertEqual(V1MongoClusterConfigurationSpecMongoDB(replicas=5), cluster_object.spec.mongodb)

def test_storage_class_name(self):
self.cluster_dict["spec"]["mongodb"]["storage_class_name"] = "fast"
self.cluster_object.spec.mongodb.storage_class_name = "fast"
self.assertEqual(self.cluster_object.to_dict(skip_validation = True),
V1MongoClusterConfiguration(**self.cluster_dict).to_dict(skip_validation = True))

def test_secret_key_ref(self):
service_account = self.cluster_object.spec.backups.gcs.service_account
expected = V1ServiceAccountRef(secret_key_ref=V1SecretKeySelector(name="storage-serviceaccount", key="json"))
self.assertEqual(expected, service_account)

def test_equals(self):
self.assertEquals(self.cluster_object, V1MongoClusterConfiguration(**self.cluster_dict))
self.assertEqual(self.cluster_object, V1MongoClusterConfiguration(**self.cluster_dict))

def test_example_repr(self):
expected = \
Expand All @@ -62,7 +67,7 @@ def test_example_repr(self):
"'prefix': 'test-backups', 'service_account': {'secret_key_ref': {'key': 'json', " \
"'name': 'storage-serviceaccount'}}}}, 'mongodb': {'cpu_limit': '100m', 'memory_limit': '64Mi', " \
"'replicas': 3}})"
self.assertEquals(expected, repr(self.cluster_object))
self.assertEqual(expected, repr(self.cluster_object))

def test_wrong_replicas(self):
self.cluster_dict["spec"]["mongodb"]["replicas"] = 2
Expand Down
44 changes: 30 additions & 14 deletions tests/services/TestKubernetesService.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ def setUp(self):
self.cluster_object = V1MongoClusterConfiguration(**self.cluster_dict)
self.name = self.cluster_object.metadata.name
self.namespace = self.cluster_object.metadata.namespace

self.stateful_set = V1beta1StatefulSet(
self.cpu_limit = "100m"
self.memory_limit = "64Mi"
self.stateful_set = self._createStatefulSet()

def _createStatefulSet(self) -> V1beta1StatefulSet:
return V1beta1StatefulSet(
metadata=self._createMeta(self.name),
spec=V1beta1StatefulSetSpec(
replicas=3,
Expand All @@ -47,15 +51,18 @@ def setUp(self):
field_ref=V1ObjectFieldSelector(api_version="v1", field_path="status.podIP")
)
)],
command=["mongod", "--replSet", self.name, "--bind_ip", "0.0.0.0", "--smallfiles",
"--noprealloc"],
command=[
"mongod",
"--wiredTigerCacheSizeGB", "0.25",
"--replSet", self.name,
"--bind_ip", "0.0.0.0",
"--smallfiles",
"--noprealloc"
],
image="mongo:3.6.4",
ports=[V1ContainerPort(name="mongodb", container_port=27017, protocol="TCP")],
volume_mounts=[V1VolumeMount(name="mongo-storage", read_only=False, mount_path="/data/db")],
resources=V1ResourceRequirements(
limits={"cpu": "100m", "memory": "64Mi"},
requests={"cpu": "100m", "memory": "64Mi"}
)
resources=self._createResourceLimits()
)])
),
volume_claim_templates=[V1PersistentVolumeClaim(
Expand All @@ -74,6 +81,12 @@ def _createMeta(self, name: str) -> V1ObjectMeta:
name=name,
namespace=self.namespace,
)

def _createResourceLimits(self) -> V1ResourceRequirements:
return V1ResourceRequirements(
limits = {"cpu": self.cpu_limit, "memory": self.memory_limit},
requests = {"cpu": self.cpu_limit, "memory": self.memory_limit}
)

def test___init__(self, client_mock):
KubernetesService()
Expand Down Expand Up @@ -112,7 +125,7 @@ def test_createMongoObjectDefinition(self, client_mock):

result = service.createMongoObjectDefinition()

self.assertEquals(expected_def, result)
self.assertEqual(expected_def, result)
expected_calls = [
call.ApiextensionsV1beta1Api().list_custom_resource_definition(),
call.ApiextensionsV1beta1Api().create_custom_resource_definition(expected_def),
Expand All @@ -129,7 +142,7 @@ def test_createMongoObjectDefinition_existing(self, client_mock):

result = service.createMongoObjectDefinition()

self.assertEquals(item, result)
self.assertEqual(item, result)

expected_calls = [call.ApiextensionsV1beta1Api().list_custom_resource_definition()]
self.assertEqual(expected_calls, client_mock.mock_calls)
Expand Down Expand Up @@ -186,8 +199,8 @@ def test_listMongoObjects_404(self, client_mock):
call.CustomObjectsApi().list_cluster_custom_object('operators.ultimaker.com', 'v1', "mongos", param='value'),
call.CustomObjectsApi().list_cluster_custom_object('operators.ultimaker.com', 'v1', "mongos", param='value'),
]
self.assertEquals(expected_calls, client_mock.mock_calls)
self.assertEquals("Could not list the custom mongo objects after 3 retries", str(context.exception))
self.assertEqual(expected_calls, client_mock.mock_calls)
self.assertEqual("Could not list the custom mongo objects after 3 retries", str(context.exception))

def test_getMongoObject(self, client_mock):
service = KubernetesService()
Expand Down Expand Up @@ -428,7 +441,10 @@ def test_createStatefulSet_no_optional_fields(self, client_mock):
client_mock.reset_mock()
del self.cluster_dict["spec"]["mongodb"]["cpu_limit"]
del self.cluster_dict["spec"]["mongodb"]["memory_limit"]
self.cpu_limit = "1"
self.memory_limit = "2Gi"
self.cluster_object = V1MongoClusterConfiguration(**self.cluster_dict)
self.stateful_set = self._createStatefulSet() # update with the new resource requirements

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

Expand Down Expand Up @@ -465,5 +481,5 @@ def test_execInPod(self, stream_mock, client_mock):
stream_mock.assert_called_once_with(client_mock.CoreV1Api.return_value.connect_get_namespaced_pod_exec,
'pod_name', 'default', command='ls', container='container',
stderr=True, stdin=False, stdout=True, tty=False)
self.assertEquals(stream_mock.return_value, result)
self.assertEquals([], client_mock.mock_calls)
self.assertEqual(stream_mock.return_value, result)
self.assertEqual([], client_mock.mock_calls)
Loading

0 comments on commit b12a80e

Please sign in to comment.