diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index d0a583c4..7fe351d9 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -496,6 +496,65 @@ def scale(self, user, structure): # noqa return False + def stop(self, user, types): # noqa + """scale containers which types contained down """ + + if self.release_set.filter(failed=False).latest().build is None: + raise DryccException('No build associated with this release') + + release = self.release_set.filter(failed=False).latest() + structure = {_: 0 for _ in types} + + # test for available process types + available_process_types = release.build.procfile or {} + for container_type in types: + if container_type == 'cmd': + continue # allow docker cmd types in case we don't have the image source + + if container_type not in available_process_types: + raise NotFound( + 'Container type {} does not exist in application'.format(container_type)) + + # merge current structure and the new items together + old_structure = self.structure + new_structure = old_structure.copy() + new_structure.update(structure) + + if new_structure != self.structure: + try: + self._scale_pods(structure) + except ServiceUnavailable: + # scaling failed, go back to old scaling numbers + self._scale_pods(old_structure) + raise + + msg = '{} stopped pods '.format(user.username) + ' '.join(types) + self.log(msg) + + return True + + return False + + def start(self, user, types): # noqa + """scale containers which types contained up.""" + # use create to make sure minimum resources are created + self.create() + if self.release_set.filter(failed=False).latest().build is None: + raise DryccException('No build associated with this release') + + structure = {} + for k, v in self.structure.items(): + if k in types: + structure[k] = v + try: + self._scale_pods(structure) + except ServiceUnavailable: + # scaling failed, go back to old scaling numbers + raise + msg = '{} stopped pods '.format(user.username) + ' '.join(types) + self.log(msg) + return True + def _scale_pods(self, scale_types): release = self.release_set.filter(failed=False).latest() app_settings = self.appsettings_set.latest() @@ -863,6 +922,7 @@ def list_pods(self, *args, **kwargs): pods = [] data = [] + exist_pod_type = [] for p in pods: labels = p['metadata']['labels'] # specifically ignore run pods @@ -889,7 +949,9 @@ def list_pods(self, *args, **kwargs): else: started = str(datetime.utcnow().strftime(settings.DRYCC_DATETIME_FORMAT)) item['started'] = started - + item['replicas'] = self.structure.get(labels['type']) + if labels['type'] not in exist_pod_type: + exist_pod_type.append(labels['type']) data.append(item) # sorting so latest start date is first diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py index a557e880..5837caec 100644 --- a/rootfs/api/serializers.py +++ b/rootfs/api/serializers.py @@ -569,11 +569,12 @@ class Meta: class PodSerializer(serializers.BaseSerializer): - name = serializers.CharField() + name = serializers.CharField(required=False) state = serializers.CharField() type = serializers.CharField() - release = serializers.CharField() - started = serializers.DateTimeField() + release = serializers.CharField(required=False) + started = serializers.DateTimeField(required=False) + replicas = serializers.IntegerField(required=False) def to_representation(self, obj): return obj diff --git a/rootfs/api/tests/test_pods.py b/rootfs/api/tests/test_pods.py index 2b306ba3..e00e6b73 100644 --- a/rootfs/api/tests/test_pods.py +++ b/rootfs/api/tests/test_pods.py @@ -100,6 +100,20 @@ def test_container_api_heroku(self, mock_requests): response = self.client.post(url, body) self.assertEqual(response.status_code, 204, response.data) + # stop + url = "/v2/apps/{app_id}/stop".format(**locals()) + # test setting one proc type at a time + body = {"types": ['web']} + response = self.client.post(url, body) + self.assertEqual(response.status_code, 204, response.data) + + # start + url = "/v2/apps/{app_id}/start".format(**locals()) + # test setting one proc type at a time + body = {"types": ['web']} + response = self.client.post(url, body) + self.assertEqual(response.status_code, 204, response.data) + url = "/v2/apps/{app_id}/pods".format(**locals()) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) @@ -121,7 +135,7 @@ def test_container_api_heroku(self, mock_requests): url = "/v2/apps/{app_id}/pods".format(**locals()) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data['results']), 0) + self.assertEqual(len(response.data['results']), 2) url = "/v2/apps/{app_id}".format(**locals()) response = self.client.get(url) @@ -191,7 +205,7 @@ def test_container_api_docker(self, mock_requests): url = "/v2/apps/{app_id}/pods".format(**locals()) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data['results']), 0) + self.assertEqual(len(response.data['results']), 1) url = "/v2/apps/{app_id}".format(**locals()) response = self.client.get(url) @@ -229,7 +243,7 @@ def test_release(self, mock_requests): url = "/v2/apps/{app_id}/pods".format(**locals()) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data['results']), 1) + self.assertEqual(len(response.data['results']), 2) self.assertEqual(response.data['results'][0]['release'], 'v2') # post a new build @@ -249,7 +263,7 @@ def test_release(self, mock_requests): url = "/v2/apps/{app_id}/pods".format(**locals()) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data['results']), 1) + self.assertEqual(len(response.data['results']), 2) self.assertEqual(response.data['results'][0]['release'], 'v3') # post new config @@ -261,7 +275,7 @@ def test_release(self, mock_requests): url = "/v2/apps/{app_id}/pods".format(**locals()) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data['results']), 1) + self.assertEqual(len(response.data['results']), 2) self.assertEqual(response.data['results'][0]['release'], 'v4') def test_container_errors(self, mock_requests): @@ -355,7 +369,7 @@ def test_pod_command_format(self, mock_requests): # verify that the app._get_command property got formatted self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data['results']), 1) + self.assertEqual(len(response.data['results']), 2) pod = response.data['results'][0] self.assertEqual(pod['type'], 'web') diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py index 8a0e7b97..e481d20b 100644 --- a/rootfs/api/urls.py +++ b/rootfs/api/urls.py @@ -57,6 +57,10 @@ # application actions url(r"^apps/(?P{})/scale/?$".format(settings.APP_URL_REGEX), views.AppViewSet.as_view({'post': 'scale'})), + url(r"^apps/(?P{})/stop/?$".format(settings.APP_URL_REGEX), + views.AppViewSet.as_view({'post': 'stop'})), + url(r"^apps/(?P{})/start/?$".format(settings.APP_URL_REGEX), + views.AppViewSet.as_view({'post': 'start'})), url(r"^apps/(?P{})/logs/?$".format(settings.APP_URL_REGEX), views.AppViewSet.as_view({'get': 'logs'})), url(r"^apps/(?P{})/run/?$".format(settings.APP_URL_REGEX), diff --git a/rootfs/api/views.py b/rootfs/api/views.py index 88f7021b..090c0131 100644 --- a/rootfs/api/views.py +++ b/rootfs/api/views.py @@ -226,6 +226,20 @@ def scale(self, request, **kwargs): self.get_object().scale(request.user, request.data) return Response(status=status.HTTP_204_NO_CONTENT) + def stop(self, request, **kwargs): + types = request.data.get("types") + if not types: + raise DryccException("types is a required field") + self.get_object().stop(request.user, types) + return Response(status=status.HTTP_204_NO_CONTENT) + + def start(self, request, **kwargs): + types = request.data.get("types") + if not types: + raise DryccException("types is a required field") + self.get_object().start(request.user, types) + return Response(status=status.HTTP_204_NO_CONTENT) + def logs(self, request, **kwargs): app = self.get_object() try: @@ -314,7 +328,23 @@ class PodViewSet(AppResourceViewSet): def list(self, *args, **kwargs): pods = self.get_app().list_pods(*args, **kwargs) data = self.get_serializer(pods, many=True).data - # fake out pagination for now + + if not kwargs.get("type"): + exist_pod_type = list(set([_["type"] for _ in data if _["type"]])) + for _ in self.get_app().structure.keys(): + if _ not in exist_pod_type: + exist_pod_type.append(_) + data.append({"type": _, + "replicas": self.get_app().structure[_], + "state": "stopped"}) + + for _ in self.get_app().procfile_structure.keys(): + if _ not in exist_pod_type: + data.append({"type": _, + "replicas": 0, + "state": "stopped"}) + + # # fake out pagination for now pagination = {'results': data, 'count': len(data)} return Response(pagination, status=status.HTTP_200_OK)