From e81a8f46e08b3c9560e00277db79dfe0ac6ee1a2 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Wed, 14 Oct 2020 13:04:46 +0800
Subject: [PATCH] feat(ps):add ps:stop/start command

---
 rootfs/api/models/app.py      | 64 ++++++++++++++++++++++++++++++++++-
 rootfs/api/serializers.py     |  7 ++--
 rootfs/api/tests/test_pods.py | 26 ++++++++++----
 rootfs/api/urls.py            |  4 +++
 rootfs/api/views.py           | 32 +++++++++++++++++-
 5 files changed, 122 insertions(+), 11 deletions(-)

diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py
index d0a583c46..7012a9ffc 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['type_rs_num'] = 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 a557e880b..f5d96dd52 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)
+    type_rs_num = 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 2b306ba3b..e00e6b73c 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 8a0e7b976..e481d20b4 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -57,6 +57,10 @@
     # application actions
     url(r"^apps/(?P<id>{})/scale/?$".format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'post': 'scale'})),
+    url(r"^apps/(?P<id>{})/stop/?$".format(settings.APP_URL_REGEX),
+        views.AppViewSet.as_view({'post': 'stop'})),
+    url(r"^apps/(?P<id>{})/start/?$".format(settings.APP_URL_REGEX),
+        views.AppViewSet.as_view({'post': 'start'})),
     url(r"^apps/(?P<id>{})/logs/?$".format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'get': 'logs'})),
     url(r"^apps/(?P<id>{})/run/?$".format(settings.APP_URL_REGEX),
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
index 88f7021b7..bcc2df6e7 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": _,
+                                 "type_rs_num": self.get_app().structure[_],
+                                 "state": "stopped"})
+
+            for _ in self.get_app().procfile_structure.keys():
+                if _ not in exist_pod_type:
+                    data.append({"type": _,
+                                 "type_rs_num": 0,
+                                 "state": "stopped"})
+
+        # # fake out pagination for now
         pagination = {'results': data, 'count': len(data)}
         return Response(pagination, status=status.HTTP_200_OK)
 
