diff --git a/magnum/api/controllers/v1/cluster.py b/magnum/api/controllers/v1/cluster.py index 4fff24661e..a2256f5c72 100755 --- a/magnum/api/controllers/v1/cluster.py +++ b/magnum/api/controllers/v1/cluster.py @@ -105,7 +105,7 @@ class Cluster(base.APIBase): default=None) """The name of the nova ssh keypair""" - node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) + node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=1) """The node count for this cluster. Default to 1 if not set""" master_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) diff --git a/magnum/api/controllers/v1/cluster_actions.py b/magnum/api/controllers/v1/cluster_actions.py index abe27068f3..0594b314f2 100644 --- a/magnum/api/controllers/v1/cluster_actions.py +++ b/magnum/api/controllers/v1/cluster_actions.py @@ -44,7 +44,7 @@ class ClusterResizeRequest(base.APIBase): This class enforces type checking and value constraints. """ - node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), mandatory=True) + node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), mandatory=True) """The expected node count after resize.""" nodes_to_remove = wsme.wsattr([wtypes.text], mandatory=False, diff --git a/magnum/api/controllers/v1/nodegroup.py b/magnum/api/controllers/v1/nodegroup.py index 4c43e6be66..260c80c38e 100644 --- a/magnum/api/controllers/v1/nodegroup.py +++ b/magnum/api/controllers/v1/nodegroup.py @@ -98,18 +98,18 @@ class NodeGroup(base.APIBase): node_addresses = wsme.wsattr([wtypes.text], readonly=True) """IP addresses of nodegroup nodes""" - node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) + node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=1) """The node count for this nodegroup. Default to 1 if not set""" role = wsme.wsattr(wtypes.StringType(min_length=1, max_length=255), default='worker') """The role of the nodes included in this nodegroup""" - min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) - """The minimum allowed nodes for this nodegroup. Default to 1 if not set""" + min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=0) + """The minimum allowed nodes for this nodegroup. Default to 0 if not set""" - max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=None) - """The maximum allowed nodes for this nodegroup. Default to 1 if not set""" + max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=None) + """The maximum allowed nodes for this nodegroup.""" is_default = types.BooleanType() """Specifies is a nodegroup was created by default or not""" diff --git a/magnum/api/controllers/v1/types.py b/magnum/api/controllers/v1/types.py index ad0a99e587..5fa6c51fe4 100644 --- a/magnum/api/controllers/v1/types.py +++ b/magnum/api/controllers/v1/types.py @@ -210,11 +210,11 @@ class JsonPatchType(wtypes.Base): raise wsme.exc.ClientSideError(msg % patch.path) if patch.op != 'remove': - if not patch.value: + if patch.value is None or patch.value == wtypes.Unset: msg = _("'add' and 'replace' operations needs value") raise wsme.exc.ClientSideError(msg) ret = {'path': patch.path, 'op': patch.op} - if patch.value: + if patch.value is not None and patch.value != wtypes.Unset: ret['value'] = patch.value return ret diff --git a/magnum/api/controllers/versions.py b/magnum/api/controllers/versions.py index b099277cca..5059d6eb4f 100644 --- a/magnum/api/controllers/versions.py +++ b/magnum/api/controllers/versions.py @@ -17,16 +17,6 @@ from webob import exc from magnum.i18n import _ -# NOTE(yuntong): v1.0 is reserved to indicate Kilo's API, but is not presently -# supported by the API service. All changes between Kilo and the -# point where we added microversioning are considered backwards- -# compatible, but are not specifically discoverable at this time. -# -# The v1.1 version indicates this "initial" version as being -# different from Kilo (v1.0), and includes the following changes: -# -# Add details of new api versions here: - # # For each newly added microversion change, update the API version history # string below with a one or two line description. Also update @@ -42,10 +32,11 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.7 - Add resize API * 1.8 - Add upgrade API * 1.9 - Add nodegroup API + * 1.10 - Allow nodegroups with 0 nodes """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.9' +CURRENT_MAX_VER = '1.10' class Version(object): diff --git a/magnum/api/rest_api_version_history.rst b/magnum/api/rest_api_version_history.rst index a9aa04b815..0dec13a43c 100644 --- a/magnum/api/rest_api_version_history.rst +++ b/magnum/api/rest_api_version_history.rst @@ -75,3 +75,12 @@ user documentation. An admin user can set/update/delete/list quotas for the given tenant. A non-admin user can get self quota information. + + +1.10 +--- + + Allow nodegroups with 0 nodes + + Allow the cluster to be created with node_count = 0 as well as to update + existing nodegroups to have 0 nodes. diff --git a/magnum/conductor/utils.py b/magnum/conductor/utils.py index 6d68bd3769..a54f96085a 100644 --- a/magnum/conductor/utils.py +++ b/magnum/conductor/utils.py @@ -18,6 +18,7 @@ from pycadf import cadftaxonomy as taxonomy from pycadf import cadftype from pycadf import eventfactory from pycadf import resource +from wsme import types as wtypes from magnum.common import clients from magnum.common import rpc @@ -176,6 +177,7 @@ def _get_nodegroup_object(context, cluster, node_count, is_master=False): ng.image_id = cluster.cluster_template.image_id ng.docker_volume_size = (cluster.docker_volume_size or cluster.cluster_template.docker_volume_size) + if is_master: ng.flavor_id = (cluster.master_flavor_id or cluster.cluster_template.master_flavor_id) @@ -183,6 +185,11 @@ def _get_nodegroup_object(context, cluster, node_count, is_master=False): else: ng.flavor_id = cluster.flavor_id or cluster.cluster_template.flavor_id ng.role = "worker" + if (cluster.labels != wtypes.Unset and cluster.labels is not None + and 'min_node_count' in cluster.labels): + ng.min_node_count = cluster.labels['min_node_count'] + else: + ng.min_node_count = 0 ng.name = "default-%s" % ng.role ng.is_default = True ng.status = fields.ClusterStatus.CREATE_IN_PROGRESS diff --git a/magnum/drivers/k8s_fedora_atomic_v1/templates/kubecluster.yaml b/magnum/drivers/k8s_fedora_atomic_v1/templates/kubecluster.yaml index 33ffa9ae06..01b95b5408 100644 --- a/magnum/drivers/k8s_fedora_atomic_v1/templates/kubecluster.yaml +++ b/magnum/drivers/k8s_fedora_atomic_v1/templates/kubecluster.yaml @@ -842,7 +842,7 @@ parameters: type: number description: > minimum node count of cluster workers when doing scale down - default: 1 + default: 0 max_node_count: type: number diff --git a/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml b/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml index 1ad80a94de..c8fd3d0c52 100644 --- a/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml +++ b/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml @@ -858,7 +858,7 @@ parameters: type: number description: > minimum node count of cluster workers when doing scale down - default: 1 + default: 0 max_node_count: type: number diff --git a/magnum/objects/nodegroup.py b/magnum/objects/nodegroup.py index b6b2e0287e..def44c6a1a 100644 --- a/magnum/objects/nodegroup.py +++ b/magnum/objects/nodegroup.py @@ -26,8 +26,9 @@ from magnum.objects import fields as m_fields class NodeGroup(base.MagnumPersistentObject, base.MagnumObject, base.MagnumObjectDictCompat): # Version 1.0: Initial version + # Version 1.1: min_node_count defaults to 0 - VERSION = '1.0' + VERSION = '1.1' dbapi = dbapi.get_instance() @@ -45,7 +46,7 @@ class NodeGroup(base.MagnumPersistentObject, base.MagnumObject, 'node_count': fields.IntegerField(nullable=False, default=1), 'role': fields.StringField(), 'max_node_count': fields.IntegerField(nullable=True), - 'min_node_count': fields.IntegerField(nullable=False, default=1), + 'min_node_count': fields.IntegerField(nullable=False, default=0), 'is_default': fields.BooleanField(default=False), 'stack_id': fields.StringField(nullable=True), 'status': m_fields.ClusterStatusField(nullable=True), diff --git a/magnum/tests/unit/api/controllers/test_root.py b/magnum/tests/unit/api/controllers/test_root.py index 94a2fbb759..9f2fa22c94 100644 --- a/magnum/tests/unit/api/controllers/test_root.py +++ b/magnum/tests/unit/api/controllers/test_root.py @@ -41,7 +41,7 @@ class TestRootController(api_base.FunctionalTest): [{u'href': u'http://localhost/v1/', u'rel': u'self'}], u'status': u'CURRENT', - u'max_version': u'1.9', + u'max_version': u'1.10', u'min_version': u'1.1'}]} self.v1_expected = { diff --git a/magnum/tests/unit/api/controllers/v1/test_cluster.py b/magnum/tests/unit/api/controllers/v1/test_cluster.py index 6ab7284f79..15d2999c6c 100644 --- a/magnum/tests/unit/api/controllers/v1/test_cluster.py +++ b/magnum/tests/unit/api/controllers/v1/test_cluster.py @@ -683,8 +683,7 @@ class TestPost(api_base.FunctionalTest): bdict['node_count'] = 0 response = self.post_json('/clusters', bdict, expect_errors=True) self.assertEqual('application/json', response.content_type) - self.assertEqual(400, response.status_int) - self.assertTrue(response.json['errors']) + self.assertEqual(202, response.status_int) def test_create_cluster_with_node_count_negative(self): bdict = apiutils.cluster_post_data() diff --git a/magnum/tests/unit/api/controllers/v1/test_nodegroup.py b/magnum/tests/unit/api/controllers/v1/test_nodegroup.py index 17594b3242..a6f73d54b2 100644 --- a/magnum/tests/unit/api/controllers/v1/test_nodegroup.py +++ b/magnum/tests/unit/api/controllers/v1/test_nodegroup.py @@ -40,7 +40,7 @@ class TestNodegroupObject(base.TestCase): del nodegroup_dict['max_node_count'] nodegroup = api_nodegroup.NodeGroup(**nodegroup_dict) self.assertEqual(1, nodegroup.node_count) - self.assertEqual(1, nodegroup.min_node_count) + self.assertEqual(0, nodegroup.min_node_count) self.assertIsNone(nodegroup.max_node_count) @@ -293,6 +293,20 @@ class TestPost(NodeGroupControllerTest): # Verify node_count defaults to 1 self.assertEqual(1, response.json['node_count']) + @mock.patch('oslo_utils.timeutils.utcnow') + def test_create_nodegroup_with_zero_nodes(self, mock_utcnow): + ng_dict = apiutils.nodegroup_post_data() + ng_dict['node_count'] = 0 + ng_dict['min_node_count'] = 0 + test_time = datetime.datetime(2000, 1, 1, 0, 0) + mock_utcnow.return_value = test_time + + response = self.post_json(self.url, ng_dict) + self.assertEqual('application/json', response.content_type) + self.assertEqual(202, response.status_int) + # Verify node_count is set to zero + self.assertEqual(0, response.json['node_count']) + @mock.patch('oslo_utils.timeutils.utcnow') def test_create_nodegroup_with_max_node_count(self, mock_utcnow): ng_dict = apiutils.nodegroup_post_data(max_node_count=5) @@ -366,7 +380,7 @@ class TestPost(NodeGroupControllerTest): self.assertEqual(self.cluster.project_id, response.json['project_id']) self.assertEqual(self.cluster.labels, response.json['labels']) self.assertEqual('worker', response.json['role']) - self.assertEqual(1, response.json['min_node_count']) + self.assertEqual(0, response.json['min_node_count']) self.assertEqual(1, response.json['node_count']) self.assertIsNone(response.json['max_node_count']) @@ -662,7 +676,7 @@ class TestPatch(NodeGroupControllerTest): response = self.get_json(self.url + self.nodegroup.uuid) # Removing the min_node_count just restores the default value - self.assertEqual(1, response['min_node_count']) + self.assertEqual(0, response['min_node_count']) return_updated_at = timeutils.parse_isotime( response['updated_at']).replace(tzinfo=None) self.assertEqual(test_time, return_updated_at) diff --git a/magnum/tests/unit/objects/test_objects.py b/magnum/tests/unit/objects/test_objects.py index e5bd4a3e3a..6ab266d312 100644 --- a/magnum/tests/unit/objects/test_objects.py +++ b/magnum/tests/unit/objects/test_objects.py @@ -364,7 +364,7 @@ object_data = { 'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c', 'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18', 'Federation': '1.0-166da281432b083f0e4b851336e12e20', - 'NodeGroup': '1.0-8cb4544a28a49860d816158a7c3060b1' + 'NodeGroup': '1.1-70211d19fcf53903a470607f1f4a784f' } diff --git a/releasenotes/notes/allow-empty-node_groups-ec16898bfc82aec0.yaml b/releasenotes/notes/allow-empty-node_groups-ec16898bfc82aec0.yaml new file mode 100644 index 0000000000..37a3cd8474 --- /dev/null +++ b/releasenotes/notes/allow-empty-node_groups-ec16898bfc82aec0.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Clusters can now be created with empty nodegroups. Existing nodegroups + can be set to node_count = 0. min_node_count defaults to 0. + This is usefull for HA or special hardware clusters with multiple + nodegroups managed by the cluster auto-scaller.