From d51fe439b8419aadad8de43389d75fe0d1d52f42 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Thu, 10 Sep 2020 17:07:58 +0800
Subject: [PATCH 01/13] feat(controller):add drycc resource cmd

---
 .../templates/controller-clusterrole.yaml     |  6 ++--
 rootfs/api/models/resource.py                 | 35 +++++++++----------
 rootfs/api/serializers.py                     |  4 +--
 rootfs/api/settings/production.py             |  8 ++---
 rootfs/api/views.py                           |  8 +++--
 rootfs/scheduler/__init__.py                  |  5 +++
 rootfs/scheduler/resources/servicecatalog.py  | 34 ++++++++++++++++--
 7 files changed, 69 insertions(+), 31 deletions(-)

diff --git a/charts/controller/templates/controller-clusterrole.yaml b/charts/controller/templates/controller-clusterrole.yaml
index 80906ae1e..4d3871d79 100644
--- a/charts/controller/templates/controller-clusterrole.yaml
+++ b/charts/controller/templates/controller-clusterrole.yaml
@@ -67,9 +67,9 @@ rules:
   verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
 - apiGroups: ["servicecatalog.k8s.io"]
   resources: ["serviceinstances"]
-  verbs: ["get", "list", "watch", "create", "delete", "update"]
+  verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
 - apiGroups: ["servicecatalog.k8s.io"]
-  resources: ["serviceinstances"]
-  verbs: ["get", "list", "watch", "create", "delete", "update"]
+  resources: ["servicebindings"]
+  verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
 {{- end -}}
 {{- end -}}
diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index b45184cc5..d4aba2bb9 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -71,7 +71,7 @@ def attach(self, *args, **kwargs):
 
     @transaction.atomic
     def delete(self, *args, **kwargs):
-        if self.binding and self.binding == "Ready":
+        if self.binding == "Ready":
             raise DryccException("the plan is still binding")
         # Deatch ServiceInstance, updates k8s
         self.detach(*args, **kwargs)
@@ -84,7 +84,7 @@ def detach(self, *args, **kwargs):
             self._scheduler.servicecatalog.get_instance(self.app.id, self.name)
             self._scheduler.servicecatalog.delete_instance(self.app.id, self.name)
         except KubeException as e:
-            raise ServiceUnavailable("Could not delete volume {} for application {}".format(name, self.app_id)) from e  # noqa
+            raise ServiceUnavailable("Could not delete resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
 
     def log(self, message, level=logging.INFO):
         """Logs a message in the context of this service.
@@ -92,10 +92,10 @@ def log(self, message, level=logging.INFO):
         This prefixes log messages with an application "tag" that the customized
         drycc-logspout will be on the lookout for.  When it's seen, the message-- usually
         an application event of some sort like releasing or scaling, will be considered
-        as "belonging" to the application instead of the controller and will be handled
+        as "belonging" to the application instead of the contoller and will be handled
         accordingly.
         """
-        logger.log(level, "[{}]: {}".format(self.id, message))
+        logger.log(level, "[{}]: {}".format(self.uuid, message))
 
     def bind(self, *args, **kwargs):
         if self.status != "Ready":
@@ -124,12 +124,14 @@ def bind(self, *args, **kwargs):
                 raise ServiceUnavailable(msg) from e
 
     def unbind(self, *args, **kwargs):
-        if self.binding != "Ready":
+        if not self.binding:
             raise DryccException("the resource is not binding")
         try:
             # We raise an exception when a resource doesn't exist
             self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
             self._scheduler.servicecatalog.delete_binding(self.app.id, self.name)
+            self.binding = None
+            self.save()
         except KubeException as e:
             raise ServiceUnavailable("Could not unbind resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
 
@@ -138,10 +140,7 @@ def attach_update(self, *args, **kwargs):
             data = self._scheduler.servicecatalog.get_instance(
                 self.app.id, self.name).json()
         except KubeException as e:
-            self.log("certificate {} does not exist".format(self.app.id),
-                     level=logging.INFO)
-            data = None
-            logger.info(e)
+            self.DryccException("resource {} does not exist".format(self.name))
         try:
             version = data["metadata"]["resourceVersion"]
             instance = self.plan.split(":")
@@ -149,10 +148,14 @@ def attach_update(self, *args, **kwargs):
                 "instance_class": instance[0],
                 "instance_plan": ":".join(instance[1:]),
                 "parameters": self.options,
+                "external_id": data["spec"]["externalID"]
             }
             self._scheduler.servicecatalog.put_instance(
                 self.app.id, self.name, version, **kwargs
             )
+            # self._scheduler.servicecatalog.patch_instance(
+            #     self.app.id, self.name, version, **kwargs
+            # )
             # create/patch/put  retrieve_task
             data = {
                 "task_id": uuid.uuid4().hex,
@@ -171,7 +174,7 @@ def retrieve(self, *args, **kwargs):
                 resp_i = self._scheduler.servicecatalog.get_instance(
                     self.app.id, self.name).json()
                 self.status = resp_i.get('status', {}).\
-                    get('lastConditionState', '').lower()
+                    get('lastConditionState')
                 update_flag = True
             except KubeException as e:
                 logger.info("retrieve instance info error: {}".format(e))
@@ -181,7 +184,7 @@ def retrieve(self, *args, **kwargs):
                 resp_b = self._scheduler.servicecatalog.get_binding(
                     self.app.id, self.name).json()
                 self.binding = resp_b.get('status', {}).\
-                    get('lastConditionState', '').lower()
+                    get('lastConditionState')
                 self.options = resp_b.get('spec', {}).get('parameters', {})
                 update_flag = True
                 secret_name = resp_b.get('spec', {}).get('secretName')
@@ -213,12 +216,8 @@ def detach_resource(self, *args, **kwargs):
                 logger.info("delete binding info error: {}".format(e))
             self.binding = None
 
-        if (self.status != "Ready") or (self.binding is None):
-            try:
-                self._scheduler.servicecatalog.delete_instance(
-                    self.app.id, self.name)
-            except KubeException as e:
-                logger.info("retrieve instance info error: {}".format(e))
+        if (self.status != "Ready") or (not self.binding):
+            self.delete()
 
 
 @task
@@ -234,7 +233,7 @@ def retrieve_task(data):
         if _:
             return True
         else:
-            t = time.time() - resource.created.timestamps()
+            t = time.time() - resource.created.timestamp()
             if t < 3600:
                 apply_async(retrieve_task, delay=30000, args=(data, ))
             elif t < 3600 * 12:
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 4b1c55b2d..e68d3bcbf 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -659,6 +659,7 @@ class ResourceSerializer(serializers.ModelSerializer):
     owner = serializers.ReadOnlyField(source='owner.username')
     name = serializers.CharField(max_length=63, required=True)
     plan = serializers.CharField(max_length=128, required=True)
+    data = JSONFieldSerializer(required=False, binary=True)
     options = JSONFieldSerializer(required=False, binary=True)
 
     class Meta:
@@ -670,8 +671,7 @@ def update(self, instance, validated_data):
         if instance.plan.split(':')[0] != validated_data.get('plan', '').split(':')[0]:  # noqa
             raise DryccException("the resource cann't changed")
         instance.plan = validated_data.get('plan')
-        if validated_data.get('options'):
-            instance.options = validated_data.get('options')
+        instance.options = validated_data.get('options')
         instance.attach_update()
         instance.save()
         return instance
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index d49395314..1a987cdca 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -392,10 +392,10 @@
     'default': {
         'ENGINE': 'django.db.backends.postgresql',
         'NAME': os.environ.get(
-            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc')),
-        'USER': os.environ.get('DRYCC_DATABASE_USER', ''),
-        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', ''),
-        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', ''),
+            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller')),
+        'USER': os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller'),
+        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', '123456'),
+        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', '192.168.6.50'),
         'PORT': os.environ.get('DRYCC_DATABASE_SERVICE_PORT', 5432),
         # https://docs.djangoproject.com/en/1.11/ref/databases/#persistent-connections
         'CONN_MAX_AGE': 600,
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
index 89e974124..afa6f9eb9 100644
--- a/rootfs/api/views.py
+++ b/rootfs/api/views.py
@@ -792,10 +792,14 @@ def binding(self, request, *args, **kwargs):
         bind_action = self.request.data.get('bind_action', '').lower()
         if bind_action == 'bind':
             resource.bind()
-            return HttpResponse(status=status.HTTP_202_ACCEPTED)
+            serializer = self.get_serializer(resource, many=False)
+            logger.info("resoruce bind response data: {}".format(serializer))
+            return Response(serializer.data)
         elif bind_action == 'unbind':
             resource.unbind()
-            return HttpResponse(status=status.HTTP_202_ACCEPTED)
+            serializer = self.get_serializer(resource, many=False)
+            logger.info("resoruce unbind response data: {}".format(serializer))
+            return Response(serializer.data)
         else:
             return Http404("unknown action")
 
diff --git a/rootfs/scheduler/__init__.py b/rootfs/scheduler/__init__.py
index 952770df6..4de862b87 100644
--- a/rootfs/scheduler/__init__.py
+++ b/rootfs/scheduler/__init__.py
@@ -221,6 +221,11 @@ def http_patch(self, path, data=None, **kwargs):
         """
         try:
             url = urljoin(self.url, path)
+            # accepted media types include:
+            # application/json-patch+json,
+            # application/merge-patch+json,
+            # application/apply-patch+yaml
+            # self.session.headers["Content-Type"] = "application/json-patch+json"
             response = self.session.patch(url, data=data, **kwargs)
         except requests.exceptions.ConnectionError as err:
             # reraise as KubeException, but log stacktrace.
diff --git a/rootfs/scheduler/resources/servicecatalog.py b/rootfs/scheduler/resources/servicecatalog.py
index 3d5d8ee44..14d5ef033 100644
--- a/rootfs/scheduler/resources/servicecatalog.py
+++ b/rootfs/scheduler/resources/servicecatalog.py
@@ -1,6 +1,9 @@
+import logging
 from scheduler.resources import Resource
 from scheduler.exceptions import KubeHTTPException
 
+logger = logging.getLogger(__name__)
+
 
 class ServiceCatalog(Resource):
     api_version = 'servicecatalog.k8s.io/v1beta1'
@@ -22,14 +25,29 @@ def service_instance_manifest(namespace, name, version=None, **kwargs):
             "spec": {
                 "clusterServiceClassExternalName": kwargs.get('instance_class'),
                 "clusterServicePlanExternalName": kwargs.get('instance_plan'),
-                #{"param - 1": "value - 1","param - 2": "value - 2"}  # noqa
-                "parameters": kwargs.get('parameters'),
             }
         }
         if version:
             data["metadata"]["resourceVersion"] = version
+        if kwargs.get('parameters'):
+            data["spec"]["parameters"] = kwargs.get('parameters')
+        if kwargs.get('external_id'):
+            data["spec"]["externalID"] = kwargs.get('external_id')
         return data
 
+    # @staticmethod
+    # def service_instance_patch_manifest(namespace, name, version=None, **kwargs):
+    #     data = {
+    #         "spec": {
+    #             "clusterServicePlanExternalName": kwargs.get('instance_plan'),
+    #             "parameters": kwargs.get('parameters'),
+    #         }
+    #     }
+    #     if version:
+    #         data["metadata"] = {}
+    #         data["metadata"]["resourceVersion"] = version
+    #     return data
+
     def get_instance(self, namespace, name=None):
         """
         Fetch a single serviceinstance or a list of serviceinstances
@@ -52,6 +70,7 @@ def create_instance(self, namespace, name, **kwargs):
         """
         url = self.api('/namespaces/{}/serviceinstances', namespace)
         data = self.service_instance_manifest(namespace, name, **kwargs)
+        logging.info("create_instance_data:{}".format(data))
         response = self.http_post(url, json=data)
         if not response.status_code == 201:
             raise KubeHTTPException(
@@ -65,6 +84,7 @@ def put_instance(self, namespace, name, version, **kwargs):
         """
         url = self.api('/namespaces/{}/serviceinstances/{}', namespace, name)
         data = self.service_instance_manifest(namespace, name, version, **kwargs)
+        logger.info("put_instance data:{}".format(data))
         response = self.http_put(url, json=data)
         if not response.status_code == 200:
             raise KubeHTTPException(
@@ -72,6 +92,16 @@ def put_instance(self, namespace, name, version, **kwargs):
                 "update serviceinstances {}".format(namespace))
         return response
 
+    # def patch_instance(self, namespace, name, version, **kwargs):
+    #     data = self.service_instance_patch_manifest(
+    #         namespace, name, version, **kwargs)
+    #     url = self.api("/namespaces/{}/serviceinstances/{}", namespace, name)
+    #     logger.info("patch_instance data:{}".format(data))
+    #     response = self.http_patch(url, json=data)
+    #     if self.unhealthy(response.status_code):
+    #         raise KubeHTTPException(response, "patch serviceinstances {}".format(namespace))
+    #     return response
+
     def delete_instance(self, namespace, name):
         """
         Delete serviceinstances

From cfe2217c706266e24399578d4b49642b1cd453f4 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Mon, 14 Sep 2020 13:26:24 +0800
Subject: [PATCH 02/13] feat(controller):add drycc resource cmd

---
 rootfs/api/models/resource.py     |  4 ++--
 rootfs/api/settings/production.py |  8 ++++----
 rootfs/api/tests/test_resource.py | 29 +++++++++++++++--------------
 3 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index d4aba2bb9..ffb0e195b 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -92,7 +92,7 @@ def log(self, message, level=logging.INFO):
         This prefixes log messages with an application "tag" that the customized
         drycc-logspout will be on the lookout for.  When it's seen, the message-- usually
         an application event of some sort like releasing or scaling, will be considered
-        as "belonging" to the application instead of the contoller and will be handled
+        as "belonging" to the application instead of the controller and will be handled
         accordingly.
         """
         logger.log(level, "[{}]: {}".format(self.uuid, message))
@@ -197,7 +197,7 @@ def retrieve(self, *args, **kwargs):
                 logger.info("retrieve binding info error: {}".format(e))
         if update_flag is True:
             self.save()
-        if self.status and self.binding:
+        if self.status == "Ready" and self.binding == "Ready":
             return True
         else:
             return False
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 97b7c7c2d..a6f3054d9 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -392,10 +392,10 @@
     'default': {
         'ENGINE': 'django.db.backends.postgresql',
         'NAME': os.environ.get(
-            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller')),
-        'USER': os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller'),
-        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', '123456'),
-        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', '192.168.6.50'),
+            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc')),
+        'USER': os.environ.get('DRYCC_DATABASE_USER', ''),
+        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', ''),
+        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', ''),
         'PORT': os.environ.get('DRYCC_DATABASE_SERVICE_PORT', 5432),
         # https://docs.djangoproject.com/en/1.11/ref/databases/#persistent-connections
         'CONN_MAX_AGE': 600,
diff --git a/rootfs/api/tests/test_resource.py b/rootfs/api/tests/test_resource.py
index 554c09d84..f41ed7173 100644
--- a/rootfs/api/tests/test_resource.py
+++ b/rootfs/api/tests/test_resource.py
@@ -102,21 +102,22 @@ def test_resource_delete(self, mock_requests):
         response = self.client.delete(url)
         self.assertEqual(response.status_code, 204)
 
-    def test_resource_update(self, mock_requests):
-        """
-        Test that resource is delete from a app
-        """
-        # create
-        app_id = self.create_app()
-        data = {'name': 'mysql', 'plan': 'mysql:5.6'}
-        self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
 
-        # Update
-        url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
-                                                          name='mysql')
-        data = {'plan': 'mysql:5.7'}
-        response = self.client.put(url, data=data)
-        self.assertEqual(response.status_code, 200)
+    # def test_resource_update(self, mock_requests):
+    #     """
+    #     Test that update resource from a app, Associated with apply_async
+    #     """
+    #     # create
+    #     app_id = self.create_app()
+    #     data = {'name': 'mysql', 'plan': 'mysql:5.6'}
+    #     self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
+    #
+    #     # Update
+    #     url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
+    #                                                       name='mysql')
+    #     data = {'plan': 'mysql:5.7'}
+    #     response = self.client.put(url, data=data)
+    #     self.assertEqual(response.status_code, 200)
 
     def test_resource_bind(self, mock_requests):
         # create

From 5eb4ee55cb963b4d2dd33c2bc90f4870a38225d4 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Mon, 14 Sep 2020 16:22:56 +0800
Subject: [PATCH 03/13] feat(controller):add drycc resource cmd

---
 rootfs/api/models/resource.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index ffb0e195b..cff5439bc 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -131,6 +131,7 @@ def unbind(self, *args, **kwargs):
             self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
             self._scheduler.servicecatalog.delete_binding(self.app.id, self.name)
             self.binding = None
+            self.data = {}
             self.save()
         except KubeException as e:
             raise ServiceUnavailable("Could not unbind resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
@@ -175,6 +176,7 @@ def retrieve(self, *args, **kwargs):
                     self.app.id, self.name).json()
                 self.status = resp_i.get('status', {}).\
                     get('lastConditionState')
+                self.options = resp_i.get('spec', {}).get('parameters', {})
                 update_flag = True
             except KubeException as e:
                 logger.info("retrieve instance info error: {}".format(e))
@@ -185,7 +187,6 @@ def retrieve(self, *args, **kwargs):
                     self.app.id, self.name).json()
                 self.binding = resp_b.get('status', {}).\
                     get('lastConditionState')
-                self.options = resp_b.get('spec', {}).get('parameters', {})
                 update_flag = True
                 secret_name = resp_b.get('spec', {}).get('secretName')
                 if secret_name:

From f909cb787ab063c7b7be51abd46322bec0cfb758 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Tue, 8 Sep 2020 11:44:37 +0800
Subject: [PATCH 04/13] style(controller): fix pep8

---
 .../templates/controller-clusterrole.yaml     |   6 +
 rootfs/api/migrations/0001_initial.py         |  21 ++
 rootfs/api/models/__init__.py                 |   2 +
 rootfs/api/models/resource.py                 | 245 ++++++++++++++++++
 rootfs/api/models/volume.py                   |  11 +
 rootfs/api/serializers.py                     |  26 ++
 rootfs/api/tests/test_resource.py             | 143 ++++++++++
 rootfs/api/urls.py                            |  11 +
 rootfs/api/views.py                           |  59 +++++
 rootfs/scheduler/mock.py                      |   6 +-
 rootfs/scheduler/resources/servicecatalog.py  | 147 +++++++++++
 rootfs/scheduler/tests/test_servicecatalog.py | 137 ++++++++++
 rootfs/tasks/task.py                          |   2 +-
 13 files changed, 813 insertions(+), 3 deletions(-)
 create mode 100644 rootfs/api/models/resource.py
 create mode 100644 rootfs/api/tests/test_resource.py
 create mode 100644 rootfs/scheduler/resources/servicecatalog.py
 create mode 100644 rootfs/scheduler/tests/test_servicecatalog.py

diff --git a/charts/controller/templates/controller-clusterrole.yaml b/charts/controller/templates/controller-clusterrole.yaml
index 4792a2c0c..80906ae1e 100644
--- a/charts/controller/templates/controller-clusterrole.yaml
+++ b/charts/controller/templates/controller-clusterrole.yaml
@@ -65,5 +65,11 @@ rules:
 - apiGroups: [""]
   resources: ["persistentvolumeclaims"]
   verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
+- apiGroups: ["servicecatalog.k8s.io"]
+  resources: ["serviceinstances"]
+  verbs: ["get", "list", "watch", "create", "delete", "update"]
+- apiGroups: ["servicecatalog.k8s.io"]
+  resources: ["serviceinstances"]
+  verbs: ["get", "list", "watch", "create", "delete", "update"]
 {{- end -}}
 {{- end -}}
diff --git a/rootfs/api/migrations/0001_initial.py b/rootfs/api/migrations/0001_initial.py
index 586554ecc..9dd73ce60 100644
--- a/rootfs/api/migrations/0001_initial.py
+++ b/rootfs/api/migrations/0001_initial.py
@@ -251,4 +251,25 @@ class Migration(migrations.Migration):
                 'unique_together': {('app', 'name')},
             },
         ),
+        migrations.CreateModel(
+            name='Resource',
+            fields=[
+                ('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('updated', models.DateTimeField(auto_now=True)),
+                ('name', models.CharField(max_length=63, validators=[api.models.validate_label])),
+                ('plan', models.CharField(max_length=128)),
+                ('data', jsonfield.fields.JSONField(blank=True, default={})),
+                ('status', models.CharField(max_length=32, null=True)),
+                ('binding', models.CharField(max_length=32, null=True)),
+                ('options', jsonfield.fields.JSONField(blank=True, default={})),
+                ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
+                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-created'],
+                'get_latest_by': 'created',
+                'unique_together': {('app', 'name')},
+            },
+        ),
     ]
diff --git a/rootfs/api/models/__init__.py b/rootfs/api/models/__init__.py
index 499e93757..180d3cb07 100644
--- a/rootfs/api/models/__init__.py
+++ b/rootfs/api/models/__init__.py
@@ -151,6 +151,7 @@ class Meta:
 from .release import Release  # noqa
 from .tls import TLS  # noqa
 from .volume import Volume  # noqa
+from .resource import Resource  # noqa
 
 # define update/delete callbacks for synchronizing
 # models with the configuration management backend
@@ -248,6 +249,7 @@ def _hook_release_created(**kwargs):
 post_delete.connect(_log_instance_removed, sender=Domain, dispatch_uid='api.models.log')
 post_delete.connect(_log_instance_removed, sender=TLS, dispatch_uid='api.models.log')
 post_delete.connect(_log_instance_removed, sender=Volume, dispatch_uid='api.models.log')
+post_delete.connect(_log_instance_removed, sender=Resource, dispatch_uid='api.models.log')
 
 
 # automatically generate a new token on creation
diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
new file mode 100644
index 000000000..b45184cc5
--- /dev/null
+++ b/rootfs/api/models/resource.py
@@ -0,0 +1,245 @@
+import logging
+import uuid
+import time
+from django.core import signals
+from django.conf import settings
+from django.db import models, transaction
+from jsonfield import JSONField
+from api.exceptions import DryccException, AlreadyExists, ServiceUnavailable
+from api.models import UuidAuditedModel, validate_label
+from scheduler import KubeException
+from tasks import task, apply_async
+
+logger = logging.getLogger(__name__)
+
+
+class Resource(UuidAuditedModel):
+    owner = models.ForeignKey(settings.AUTH_USER_MODEL,
+                              on_delete=models.PROTECT)
+    app = models.ForeignKey('App', on_delete=models.CASCADE)
+    name = models.CharField(max_length=63, validators=[validate_label])
+    plan = models.CharField(max_length=128)
+    data = JSONField(default={}, blank=True)
+    status = models.CharField(max_length=32, null=True)
+    binding = models.CharField(max_length=32, null=True)
+    options = JSONField(default={}, blank=True)
+
+    class Meta:
+        get_latest_by = 'created'
+        unique_together = (('app', 'name'),)
+        ordering = ['-created']
+
+    def __str__(self):
+        return self.name
+
+    @transaction.atomic
+    def save(self, *args, **kwargs):
+        # Attach ServiceInstance, updates k8s
+        if self.created == self.updated:
+            self.attach(*args, **kwargs)
+        # Save to DB
+        return super(Resource, self).save(*args, **kwargs)
+
+    def attach(self, *args, **kwargs):
+        try:
+            self._scheduler.servicecatalog.get_instance(self.app.id, self.name)
+            err = "Resource {} already exists in this namespace".format(self.name)  # noqa
+            self.log(err, logging.INFO)
+            raise AlreadyExists(err)
+        except KubeException as e:
+            logger.info(e)
+            try:
+                instance = self.plan.split(":")
+                kwargs = {
+                    "instance_class": instance[0],
+                    "instance_plan": ":".join(instance[1:]),
+                    "parameters": self.options,
+                }
+                self._scheduler.servicecatalog.create_instance(
+                    self.app.id, self.name, **kwargs
+                )
+                # create/patch/put  retrieve_task
+                data = {
+                    "task_id": uuid.uuid4().hex,
+                    "resource_id": str(self.uuid),
+                }
+                apply_async(retrieve_task, delay=30000, args=(data, ))
+            except KubeException as e:
+                msg = 'There was a problem creating the resource ' \
+                      '{} for {}'.format(self.name, self.app_id)
+                raise ServiceUnavailable(msg) from e
+
+    @transaction.atomic
+    def delete(self, *args, **kwargs):
+        if self.binding and self.binding == "Ready":
+            raise DryccException("the plan is still binding")
+        # Deatch ServiceInstance, updates k8s
+        self.detach(*args, **kwargs)
+        # Delete from DB
+        return super(Resource, self).delete(*args, **kwargs)
+
+    def detach(self, *args, **kwargs):
+        try:
+            # We raise an exception when a resource doesn't exist
+            self._scheduler.servicecatalog.get_instance(self.app.id, self.name)
+            self._scheduler.servicecatalog.delete_instance(self.app.id, self.name)
+        except KubeException as e:
+            raise ServiceUnavailable("Could not delete volume {} for application {}".format(name, self.app_id)) from e  # noqa
+
+    def log(self, message, level=logging.INFO):
+        """Logs a message in the context of this service.
+
+        This prefixes log messages with an application "tag" that the customized
+        drycc-logspout will be on the lookout for.  When it's seen, the message-- usually
+        an application event of some sort like releasing or scaling, will be considered
+        as "belonging" to the application instead of the controller and will be handled
+        accordingly.
+        """
+        logger.log(level, "[{}]: {}".format(self.id, message))
+
+    def bind(self, *args, **kwargs):
+        if self.status != "Ready":
+            raise DryccException("the resource is not ready")
+        if self.binding == "Ready":
+            raise DryccException("the resource is binding")
+        try:
+            self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
+            err = "Resource {} is binding".format(self.name)
+            self.log(err, logging.INFO)
+            raise AlreadyExists(err)
+        except KubeException as e:
+            logger.info(e)
+            try:
+                self._scheduler.servicecatalog.create_binding(
+                    self.app.id, self.name, **kwargs)
+                # create/patch/put  retrieve_task
+                data = {
+                    "task_id": uuid.uuid4().hex,
+                    "resource_id": str(self.uuid),
+                }
+                apply_async(retrieve_task, delay=30000, args=(data, ))
+            except KubeException as e:
+                msg = 'There was a problem binding the resource ' \
+                      '{} for {}'.format(self.name, self.app_id)
+                raise ServiceUnavailable(msg) from e
+
+    def unbind(self, *args, **kwargs):
+        if self.binding != "Ready":
+            raise DryccException("the resource is not binding")
+        try:
+            # We raise an exception when a resource doesn't exist
+            self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
+            self._scheduler.servicecatalog.delete_binding(self.app.id, self.name)
+        except KubeException as e:
+            raise ServiceUnavailable("Could not unbind resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
+
+    def attach_update(self, *args, **kwargs):
+        try:
+            data = self._scheduler.servicecatalog.get_instance(
+                self.app.id, self.name).json()
+        except KubeException as e:
+            self.log("certificate {} does not exist".format(self.app.id),
+                     level=logging.INFO)
+            data = None
+            logger.info(e)
+        try:
+            version = data["metadata"]["resourceVersion"]
+            instance = self.plan.split(":")
+            kwargs = {
+                "instance_class": instance[0],
+                "instance_plan": ":".join(instance[1:]),
+                "parameters": self.options,
+            }
+            self._scheduler.servicecatalog.put_instance(
+                self.app.id, self.name, version, **kwargs
+            )
+            # create/patch/put  retrieve_task
+            data = {
+                "task_id": uuid.uuid4().hex,
+                "resource_id": str(self.uuid),
+            }
+            apply_async(retrieve_task, delay=30000, args=(data, ))
+        except KubeException as e:
+            msg = 'There was a problem update the resource ' \
+                  '{} for {}'.format(self.name, self.app_id)
+            raise ServiceUnavailable(msg) from e
+
+    def retrieve(self, *args, **kwargs):
+        update_flag = False
+        if self.status != "Ready":
+            try:
+                resp_i = self._scheduler.servicecatalog.get_instance(
+                    self.app.id, self.name).json()
+                self.status = resp_i.get('status', {}).\
+                    get('lastConditionState', '').lower()
+                update_flag = True
+            except KubeException as e:
+                logger.info("retrieve instance info error: {}".format(e))
+        if self.binding != "Ready":
+            try:
+                # We raise an exception when a resource doesn't exist
+                resp_b = self._scheduler.servicecatalog.get_binding(
+                    self.app.id, self.name).json()
+                self.binding = resp_b.get('status', {}).\
+                    get('lastConditionState', '').lower()
+                self.options = resp_b.get('spec', {}).get('parameters', {})
+                update_flag = True
+                secret_name = resp_b.get('spec', {}).get('secretName')
+                if secret_name:
+                    resp_s = self._scheduler.secret.get(
+                        self.app.id, secret_name).json()
+                    self.data = resp_s.get('data', {})
+                    update_flag = True
+            except KubeException as e:
+                logger.info("retrieve binding info error: {}".format(e))
+        if update_flag is True:
+            self.save()
+        if self.status and self.binding:
+            return True
+        else:
+            return False
+
+    def detach_resource(self, *args, **kwargs):
+        if self.binding != "Ready":
+            try:
+                resp_b = self._scheduler.servicecatalog.get_binding(
+                    self.app.id, self.name).json()
+                secret_name = resp_b.get('spec', {}).get('secretName')
+                if secret_name:
+                    self._scheduler.secret.delete(self.app.id, secret_name)
+                self._scheduler.servicecatalog.delete_binding(
+                    self.app.id, self.name)
+            except KubeException as e:
+                logger.info("delete binding info error: {}".format(e))
+            self.binding = None
+
+        if (self.status != "Ready") or (self.binding is None):
+            try:
+                self._scheduler.servicecatalog.delete_instance(
+                    self.app.id, self.name)
+            except KubeException as e:
+                logger.info("retrieve instance info error: {}".format(e))
+
+
+@task
+def retrieve_task(data):
+    try:
+        signals.request_started.send(sender=data['task_id'])
+        try:
+            resource = Resource.objects.get(uuid=data['resource_id'])
+        except Resource.DoesNotExist:
+            logger.info("retrieve task not found resource: {}".format(data['resource_id']))  # noqa
+            return True
+        _ = resource.retrieve()
+        if _:
+            return True
+        else:
+            t = time.time() - resource.created.timestamps()
+            if t < 3600:
+                apply_async(retrieve_task, delay=30000, args=(data, ))
+            elif t < 3600 * 12:
+                apply_async(retrieve_task, delay=1800000, args=(data, ))
+            else:
+                resource.detach_resource()
+    finally:
+        signals.request_finished.send(sender=data['task_id'])
diff --git a/rootfs/api/models/volume.py b/rootfs/api/models/volume.py
index d0150f712..a15473c47 100644
--- a/rootfs/api/models/volume.py
+++ b/rootfs/api/models/volume.py
@@ -68,3 +68,14 @@ def detach(self, *args, **kwargs):
         except KubeException as e:
             raise ServiceUnavailable("Could not delete volume {} for application \
                 {}".format(name, self.app_id)) from e  # noqa
+
+    def log(self, message, level=logging.INFO):
+        """Logs a message in the context of this service.
+
+        This prefixes log messages with an application "tag" that the customized
+        drycc-logspout will be on the lookout for.  When it's seen, the message-- usually
+        an application event of some sort like releasing or scaling, will be considered
+        as "belonging" to the application instead of the controller and will be handled
+        accordingly.
+        """
+        logger.log(level, "[{}]: {}".format(self.id, message))
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index e103dde1d..4b1c55b2d 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -19,6 +19,8 @@
 
 # proc type name is lowercase alphanumeric
 # https://docs-v2.readthedocs.io/en/latest/using-workflow/process-types-and-the-procfile/#declaring-process-types
+from api.exceptions import DryccException
+
 PROCTYPE_MATCH = re.compile(r'^(?P<type>[a-z0-9]+(\-[a-z0-9]+)*)$')
 PROCTYPE_MISMATCH_MSG = "Process types can only contain lowercase alphanumeric characters"
 MEMLIMIT_MATCH = re.compile(
@@ -649,3 +651,27 @@ def validate_path(self, data):
                 raise serializers.ValidationError(
                     "Volume path format: /path")
         return data
+
+
+class ResourceSerializer(serializers.ModelSerializer):
+    """Serialize a :class:`~api.models.Resource` model."""
+    app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all())
+    owner = serializers.ReadOnlyField(source='owner.username')
+    name = serializers.CharField(max_length=63, required=True)
+    plan = serializers.CharField(max_length=128, required=True)
+    options = JSONFieldSerializer(required=False, binary=True)
+
+    class Meta:
+        """Metadata options for a :class:`ResourceSerializer`."""
+        model = models.Resource
+        fields = '__all__'
+
+    def update(self, instance, validated_data):
+        if instance.plan.split(':')[0] != validated_data.get('plan', '').split(':')[0]:  # noqa
+            raise DryccException("the resource cann't changed")
+        instance.plan = validated_data.get('plan')
+        if validated_data.get('options'):
+            instance.options = validated_data.get('options')
+        instance.attach_update()
+        instance.save()
+        return instance
diff --git a/rootfs/api/tests/test_resource.py b/rootfs/api/tests/test_resource.py
new file mode 100644
index 000000000..554c09d84
--- /dev/null
+++ b/rootfs/api/tests/test_resource.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+"""
+Unit tests for the Drycc api app.
+
+Run the tests with "./manage.py test api"
+"""
+from django.contrib.auth.models import User
+from django.core.cache import cache
+from django.conf import settings
+from rest_framework.authtoken.models import Token
+from api.tests import adapter, DryccTransactionTestCase
+import requests_mock
+
+
+@requests_mock.Mocker(real_http=True, adapter=adapter)
+class ResourceTest(DryccTransactionTestCase):
+    """Tests setting and updating config values"""
+
+    fixtures = ['tests.json']
+
+    def setUp(self):
+        self.user = User.objects.get(username='autotest')
+        self.token = Token.objects.get(user=self.user).key
+        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
+        self.app_id = self.create_app()
+
+    def tearDown(self):
+        # Restore default tags to empty string
+        settings.DRYCC_DEFAULT_CONFIG_TAGS = ''
+        # make sure every test has a clean slate for k8s mocking
+        cache.clear()
+
+    def test_resources_create(self, mock_requests):
+        """Test that the serialized response contains only relevant data."""
+        app_id = self.create_app()
+
+        response = self.client.post(
+            '/v2/apps/{}/resources'.format(app_id),
+            data={'name': 'mysql', 'plan': 'mysql:5.6'}
+        )
+        self.assertEqual(response.status_code, 201, response.data)
+
+        for key in response.data:
+            self.assertIn(key,
+                          ['uuid', 'owner', 'created', 'updated', 'app', 'plan',
+                           'options', 'data', 'status', 'binding', 'name'])
+
+        expected = {
+            'owner': self.user.username,
+            'app': app_id,
+            'name': 'mysql',
+            'plan': 'mysql:5.6'
+        }
+        self.assertDictContainsSubset(expected, response.data)
+
+    def test_resources_list(self, mock_requests):
+        """
+        Test that list resources from a app
+        """
+        # create
+        app_id = self.create_app()
+        data = [
+            {'name': 'mysql', 'plan': 'mysql:5.6'}
+        ]
+        for _ in data:
+            self.client.post('/v2/apps/{}/resources'.format(app_id), data=_)
+        # Fetch
+        url = '/v2/apps/{app_id}/resources'.format(app_id=app_id)
+        response = self.client.get(url)
+        expected = [res['name'] for res in response.data['results']]
+        self.assertEqual(sorted([_['name'] for _ in data]), sorted(expected))
+
+    def test_resource_get(self, mock_requests):
+        """
+        Test that resource is get detail from a app
+        """
+        # create
+        app_id = self.create_app()
+        data = {'name': 'mysql', 'plan': 'mysql:5.6'}
+        self.client.post('/v2/apps/{}/resources/'.format(app_id), data=data)
+        # Get
+        url = '/v2/apps/{app_id}/resources/{name}/'.format(app_id=app_id,
+                                                           name='mysql')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+    def test_resource_delete(self, mock_requests):
+        """
+        Test that resource is delete from a app
+        """
+        # create
+        app_id = self.create_app()
+        data = [
+            {'name': 'mysql', 'plan': 'mysql:5.6'}
+        ]
+        for _ in data:
+            self.client.post('/v2/apps/{}/resources'.format(app_id), data=_)
+
+        # Delete
+        url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
+                                                          name='mysql')
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, 204)
+
+    def test_resource_update(self, mock_requests):
+        """
+        Test that resource is delete from a app
+        """
+        # create
+        app_id = self.create_app()
+        data = {'name': 'mysql', 'plan': 'mysql:5.6'}
+        self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
+
+        # Update
+        url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
+                                                          name='mysql')
+        data = {'plan': 'mysql:5.7'}
+        response = self.client.put(url, data=data)
+        self.assertEqual(response.status_code, 200)
+
+    def test_resource_bind(self, mock_requests):
+        # create
+        app_id = self.create_app()
+        data = {'name': 'mysql', 'plan': 'mysql:5.6'}
+        self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
+        # bind
+        url = '/v2/apps/{app_id}/resources/mysql/binding/'.format(app_id=app_id)
+        data = {"bind_action": "bind"}
+        response = self.client.patch(url, data=data)
+        # expected = response.data['path']
+        self.assertEqual(response.status_code, 400)
+
+    def test_resource_unbind(self, mock_requests):
+        # create
+        app_id = self.create_app()
+        data = {'name': 'mysql', 'plan': 'mysql:5.6'}
+        self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
+        # unbind
+        url = '/v2/apps/{app_id}/resources/mysql/binding/'.format(app_id=app_id)
+        data = {"bind_action": "unbind"}
+        response = self.client.patch(url, data=data)
+        # expected = response.data['path']
+        self.assertEqual(response.status_code, 400)
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index 936e214f1..855e15dde 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -82,6 +82,17 @@
         views.AppVolumesViewSet.as_view({'delete': 'destroy'})),
     url(r"^apps/(?P<id>{})/volumes/(?P<name>[-_\w]+)/path/?$".format(settings.APP_URL_REGEX),
         views.AppVolumeMountPathViewSet.as_view({'patch': 'path'})),
+    # application resources
+    url(r"^apps/(?P<id>{})/resources/?$".format(settings.APP_URL_REGEX),
+        views.AppResourcesViewSet.as_view({'get': 'list', 'post': 'create'})),
+    url(r"^apps/(?P<id>{})/resources/(?P<name>[-_\w]+)/?$".format(settings.APP_URL_REGEX),
+        views.AppSingleResourceViewSet.as_view({
+            'get': 'retrieve',
+            'delete': 'destroy',
+            'put': 'update'
+        })),
+    url(r"^apps/(?P<id>{})/resources/(?P<name>[-_\w]+)/binding/?$".format(settings.APP_URL_REGEX),
+        views.AppResourceBindingViewSet.as_view({'patch': 'binding'})),
     # apps base endpoint
     url(r"^apps/(?P<id>{})/?$".format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'get': 'retrieve', 'post': 'update', 'delete': 'destroy'})),
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
index 77e322cae..89e974124 100644
--- a/rootfs/api/views.py
+++ b/rootfs/api/views.py
@@ -741,6 +741,65 @@ def deploy(self, volume, pre_mount_path):
             raise DryccException(str(e)) from e
 
 
+class AppResourcesViewSet(AppResourceViewSet):
+    """RESTful views for resources apps with collaborators."""
+    model = models.Resource
+    serializer_class = serializers.ResourceSerializer
+
+
+class AppSingleResourceViewSet(AppResourceViewSet):
+    """RESTful views for resource apps with collaborators."""
+    model = models.Resource
+    serializer_class = serializers.ResourceSerializer
+
+    def get_object(self):
+        return get_object_or_404(models.Resource,
+                                 app__id=self.kwargs['id'],
+                                 name=self.kwargs['name'])
+
+    def retrieve(self, request, *args, **kwargs):
+        resource = self.get_object()
+        resource.retrieve(request)
+        return super(AppSingleResourceViewSet, self).retrieve(request, *args, **kwargs)  # noqa
+
+    def destroy(self, request, *args, **kwargs):
+        resource = self.get_object()
+        resource.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+    def update(self, request, *args, **kwargs):
+        resource = self.get_object()
+        resource = serializers.ResourceSerializer(data=request.data,
+                                                  instance=resource,
+                                                  partial=True)
+        if resource.is_valid():
+            resource.save()
+        return Response(resource.data)
+
+
+class AppResourceBindingViewSet(AppResourceViewSet):
+    model = models.Resource
+    serializer_class = serializers.ResourceSerializer
+
+    def get_object(self):
+        return get_object_or_404(models.Resource,
+                                 app__id=self.kwargs['id'],
+                                 name=self.kwargs['name'])
+
+    def binding(self, request, *args, **kwargs):
+        resource = self.get_object()
+        # {"bind_action":bind/unbind}
+        bind_action = self.request.data.get('bind_action', '').lower()
+        if bind_action == 'bind':
+            resource.bind()
+            return HttpResponse(status=status.HTTP_202_ACCEPTED)
+        elif bind_action == 'unbind':
+            resource.unbind()
+            return HttpResponse(status=status.HTTP_202_ACCEPTED)
+        else:
+            return Http404("unknown action")
+
+
 class AdminPermsViewSet(BaseDryccViewSet):
     """RESTful views for sharing admin permissions with other users."""
 
diff --git a/rootfs/scheduler/mock.py b/rootfs/scheduler/mock.py
index d734116ee..602881e73 100644
--- a/rootfs/scheduler/mock.py
+++ b/rootfs/scheduler/mock.py
@@ -80,7 +80,8 @@ def _acquire(self):
 resources = [
     'namespaces', 'nodes', 'pods', 'replicationcontrollers',
     'secrets', 'services', 'events', 'deployments', 'replicasets',
-    'horizontalpodautoscalers', 'scale', 'resourcequotas', 'ingresses', 'persistentvolumeclaims'
+    'horizontalpodautoscalers', 'scale', 'resourcequotas', 'ingresses',
+    'persistentvolumeclaims', "serviceinstances", "servicebindings"
 ]
 
 
@@ -122,7 +123,8 @@ def get_type(key):
 def replace_api_version(old, new='api_v1'):
     return old.replace('apis_autoscaling_v1', new)\
         .replace('apis_apps_v1', new)\
-        .replace('apis_networking.k8s.io_v1beta1', new)
+        .replace('apis_networking.k8s.io_v1beta1', new)\
+        .replace('apis_servicecatalog.k8s.io_v1beta1', new)
 
 
 def get_namespace(url, resource_type):
diff --git a/rootfs/scheduler/resources/servicecatalog.py b/rootfs/scheduler/resources/servicecatalog.py
new file mode 100644
index 000000000..3d5d8ee44
--- /dev/null
+++ b/rootfs/scheduler/resources/servicecatalog.py
@@ -0,0 +1,147 @@
+from scheduler.resources import Resource
+from scheduler.exceptions import KubeHTTPException
+
+
+class ServiceCatalog(Resource):
+    api_version = 'servicecatalog.k8s.io/v1beta1'
+    api_prefix = 'apis'
+
+    @staticmethod
+    def service_instance_manifest(namespace, name, version=None, **kwargs):
+        labels = {
+            'heritage': 'drycc',
+        }
+        data = {
+            "apiVersion": 'servicecatalog.k8s.io/v1beta1',
+            "kind": "ServiceInstance",
+            "metadata": {
+                "name": name,
+                "namespace": namespace,
+                'labels': labels
+            },
+            "spec": {
+                "clusterServiceClassExternalName": kwargs.get('instance_class'),
+                "clusterServicePlanExternalName": kwargs.get('instance_plan'),
+                #{"param - 1": "value - 1","param - 2": "value - 2"}  # noqa
+                "parameters": kwargs.get('parameters'),
+            }
+        }
+        if version:
+            data["metadata"]["resourceVersion"] = version
+        return data
+
+    def get_instance(self, namespace, name=None):
+        """
+        Fetch a single serviceinstance or a list of serviceinstances
+        """
+        if name is not None:
+            url = self.api('/namespaces/{}/serviceinstances/{}',
+                           namespace, name)
+            message = 'get serviceinstances ' + name
+        else:
+            url = self.api('/namespaces/{}/serviceinstances', namespace)
+            message = 'get serviceinstances'
+        response = self.http_get(url)
+        if self.unhealthy(response.status_code):
+            raise KubeHTTPException(response, message)
+        return response
+
+    def create_instance(self, namespace, name, **kwargs):
+        """
+        Create serviceinstances
+        """
+        url = self.api('/namespaces/{}/serviceinstances', namespace)
+        data = self.service_instance_manifest(namespace, name, **kwargs)
+        response = self.http_post(url, json=data)
+        if not response.status_code == 201:
+            raise KubeHTTPException(
+                response,
+                "create serviceinstances {}".format(namespace))
+        return response
+
+    def put_instance(self, namespace, name, version, **kwargs):
+        """
+        update serviceinstances
+        """
+        url = self.api('/namespaces/{}/serviceinstances/{}', namespace, name)
+        data = self.service_instance_manifest(namespace, name, version, **kwargs)
+        response = self.http_put(url, json=data)
+        if not response.status_code == 200:
+            raise KubeHTTPException(
+                response,
+                "update serviceinstances {}".format(namespace))
+        return response
+
+    def delete_instance(self, namespace, name):
+        """
+        Delete serviceinstances
+        """
+        url = self.api('/namespaces/{}/serviceinstances/{}', namespace,
+                       name)
+        response = self.http_delete(url)
+        if self.unhealthy(response.status_code):
+            raise KubeHTTPException(response, 'delete serviceinstance ' + name)
+        return response
+
+    @staticmethod
+    def service_binding_manifest(namespace, name, version=None, **kwargs):
+        labels = {
+            'heritage': 'drycc',
+        }
+        data = {
+            "apiVersion": 'servicecatalog.k8s.io/v1beta1',
+            "kind": "ServiceBinding",
+            "metadata": {
+                "name": name,
+                "namespace": namespace,
+                'labels': labels
+            },
+            "spec": {
+                "instanceRef": {
+                    "name": name
+                }
+            }
+        }
+        if version:
+            data["metadata"]["resourceVersion"] = version
+        return data
+
+    def get_binding(self, namespace, name=None):
+        """
+        Fetch a single servicebinding or a list of servicebindings
+        """
+        if name is not None:
+            url = self.api('/namespaces/{}/servicebindings/{}',
+                           namespace, name)
+            message = 'get servicebindings ' + name
+        else:
+            url = self.api('/namespaces/{}/servicebindings', namespace)
+            message = 'get servicebindings'
+        response = self.http_get(url)
+        if self.unhealthy(response.status_code):
+            raise KubeHTTPException(response, message)
+        return response
+
+    def create_binding(self, namespace, name, **kwargs):
+        """
+        Create servicebindings
+        """
+        url = self.api('/namespaces/{}/servicebindings', namespace)
+        data = self.service_binding_manifest(namespace, name, **kwargs)
+        response = self.http_post(url, json=data)
+        if not response.status_code == 201:
+            raise KubeHTTPException(
+                response,
+                "create servicebindings {}".format(namespace))
+        return response
+
+    def delete_binding(self, namespace, name):
+        """
+        Delete servicebindings
+        """
+        url = self.api('/namespaces/{}/servicebindings/{}', namespace, name)
+        response = self.http_delete(url)
+        if self.unhealthy(response.status_code):
+            raise KubeHTTPException(response,
+                                    'delete servicebindings ' + name)
+        return response
diff --git a/rootfs/scheduler/tests/test_servicecatalog.py b/rootfs/scheduler/tests/test_servicecatalog.py
new file mode 100644
index 000000000..f8e9fcdcd
--- /dev/null
+++ b/rootfs/scheduler/tests/test_servicecatalog.py
@@ -0,0 +1,137 @@
+"""
+Unit tests for the Drycc scheduler module.
+
+Run the tests with './manage.py test scheduler'
+"""
+from scheduler import KubeHTTPException
+from scheduler.tests import TestCase
+from scheduler.utils import generate_random_name
+
+
+class ServiceCatalogTest(TestCase):
+
+    def create_instince(self, namespace=None, name=generate_random_name(),
+                        **kwargs):
+        """
+        Helper function to create and verify a serviceinstances on the namespace
+        """
+        namespace = self.namespace if namespace is None else namespace
+        # these are all required even if it is kwargs...
+        kwargs = {
+            "instance_class": "server",
+            "instance_plan": "1-1",
+            "parameters": {
+                "param-1": "value-1",
+                "param-2": "value-2"
+            }
+        }
+        instance = self.scheduler.servicecatalog.create_instance(namespace,
+                                                                 name, **kwargs)
+        self.assertEqual(instance.status_code, 201, instance.json())
+        return name
+
+    def test_create_instince_failure(self):
+        with self.assertRaises(
+            KubeHTTPException,
+            msg='failed to create serviceinstances doesnotexist in Namespace \
+            {}: 404 Not Found'.format(
+                self.namespace)  # noqa
+        ):
+            self.create_instince('doesnotexist', 'doesnotexist')
+
+    def test_create_instance(self):
+        self.scheduler.ns.create("test-serviceinstance")
+        self.create_instince(namespace="test-serviceinstance")
+
+    def test_delete_instance_failure(self):
+        # test failure
+        with self.assertRaises(
+            KubeHTTPException,
+            msg='failed to delete serviceinstance foo in Namespace {}: 404 Not Found'.format(self.namespace)  # noqa
+        ):
+            self.scheduler.servicecatalog.delete_instance(self.namespace, 'foo')
+
+    def test_instince_delete(self):
+        # test success
+        name = self.create_instince()
+        response = self.scheduler.servicecatalog.delete_instance(self.namespace,
+                                                                 name)
+        data = response.json()
+        self.assertEqual(response.status_code, 200, data)
+
+    def test_get_serviceinstances(self):
+        # test success
+        name = self.create_instince()
+        response = self.scheduler.servicecatalog.get_instance(self.namespace)
+        data = response.json()
+        self.assertEqual(response.status_code, 200, data)
+        self.assertIn('items', data)
+        self.assertEqual(1, len(data['items']), data['items'])
+        # simple verify of data
+        self.assertEqual(data['items'][0]['metadata']['name'], name, data)
+
+    def test_get_serviceinstance(self):
+        # test success
+        name = self.create_instince()
+        response = self.scheduler.servicecatalog.get_instance(self.namespace, name)
+        data = response.json()
+        self.assertEqual(response.status_code, 200, data)
+        # simple verify of data
+        self.assertEqual(data['metadata']['name'], name, data)
+
+    def test_get_serviceinstances_failure(self):
+        # test failure
+        with self.assertRaises(
+            KubeHTTPException,
+            msg='failed to get Pod doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace)  # noqa
+        ):
+            self.scheduler.servicecatalog.get_instance(self.namespace,
+                                                       'doesnotexist')
+
+    def create_binding(self, namespace=None, name=generate_random_name(),
+                       **kwargs):
+        name = self.create_instince()
+        # these are all required even if it is kwargs...
+        instance = self.scheduler.servicecatalog.create_binding(self.namespace,
+                                                                name)
+        self.assertEqual(instance.status_code, 201, instance.json())
+        return name
+
+    def test_create_binding(self):
+        self.scheduler.ns.create("test-serviceinstance")
+        self.create_binding(namespace="test-serviceinstance")
+
+    # def test_create_binding_failure(self):
+    #     with self.assertRaises(
+    #         KubeHTTPException,
+    #         msg='failed to create servicebindings doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa
+    #     ):
+    #         self.create_binding(self.namespace, 'doesnotexist')
+
+    def test_binding_delete(self):
+        # test success
+        name = self.create_binding(namespace="test-serviceinstance")
+        response = self.scheduler.servicecatalog.delete_binding(self.namespace,
+                                                                name)
+        data = response.json()
+        self.assertEqual(response.status_code, 200, data)
+
+    def test_get_servicebindings(self):
+        # test success
+        name = self.create_binding()
+        response = self.scheduler.servicecatalog.get_binding(self.namespace)
+        data = response.json()
+        self.assertEqual(response.status_code, 200, data)
+        self.assertIn('items', data)
+        self.assertEqual(1, len(data['items']), data['items'])
+        # simple verify of data
+        self.assertEqual(data['items'][0]['metadata']['name'], name, data)
+
+    def test_get_servicebindings_failure(self):
+        # test failure
+        with self.assertRaises(
+            KubeHTTPException,
+            msg='failed to get Pod doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace)  # noqa
+        ):
+            self.scheduler.servicecatalog.get_binding(self.namespace,
+                                                      'doesnotexist')
diff --git a/rootfs/tasks/task.py b/rootfs/tasks/task.py
index a7c893779..8d04525fb 100644
--- a/rootfs/tasks/task.py
+++ b/rootfs/tasks/task.py
@@ -51,7 +51,7 @@ def apply_async(target, delay=0, callback=None, args=(), kwargs=None):
     message = json.dumps({
         "target_id": target_id,
         "args": args,
-        "kwargs": {} if kwargs == None else kwargs
+        "kwargs": {} if kwargs is None else kwargs
     }).encode("utf-8")
     if delay <= 0:
         NSQD_WRITER.pub(NSQ_TOPIC, message, callback=callback)

From 97e2f7532514eefad25a3af8ae9a1f232fc2bfe1 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 10 Sep 2020 14:49:19 +0800
Subject: [PATCH 05/13] fix(controller): bump tornado 5.1.1

---
 .../templates/controller-clusterrole.yaml     |  6 ++--
 rootfs/api/models/resource.py                 | 35 +++++++++----------
 rootfs/api/serializers.py                     |  4 +--
 rootfs/api/settings/production.py             |  8 ++---
 rootfs/api/views.py                           |  8 +++--
 rootfs/requirements.txt                       |  1 +
 rootfs/scheduler/__init__.py                  |  5 +++
 rootfs/scheduler/resources/servicecatalog.py  | 34 ++++++++++++++++--
 8 files changed, 70 insertions(+), 31 deletions(-)

diff --git a/charts/controller/templates/controller-clusterrole.yaml b/charts/controller/templates/controller-clusterrole.yaml
index 80906ae1e..4d3871d79 100644
--- a/charts/controller/templates/controller-clusterrole.yaml
+++ b/charts/controller/templates/controller-clusterrole.yaml
@@ -67,9 +67,9 @@ rules:
   verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
 - apiGroups: ["servicecatalog.k8s.io"]
   resources: ["serviceinstances"]
-  verbs: ["get", "list", "watch", "create", "delete", "update"]
+  verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
 - apiGroups: ["servicecatalog.k8s.io"]
-  resources: ["serviceinstances"]
-  verbs: ["get", "list", "watch", "create", "delete", "update"]
+  resources: ["servicebindings"]
+  verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
 {{- end -}}
 {{- end -}}
diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index b45184cc5..d4aba2bb9 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -71,7 +71,7 @@ def attach(self, *args, **kwargs):
 
     @transaction.atomic
     def delete(self, *args, **kwargs):
-        if self.binding and self.binding == "Ready":
+        if self.binding == "Ready":
             raise DryccException("the plan is still binding")
         # Deatch ServiceInstance, updates k8s
         self.detach(*args, **kwargs)
@@ -84,7 +84,7 @@ def detach(self, *args, **kwargs):
             self._scheduler.servicecatalog.get_instance(self.app.id, self.name)
             self._scheduler.servicecatalog.delete_instance(self.app.id, self.name)
         except KubeException as e:
-            raise ServiceUnavailable("Could not delete volume {} for application {}".format(name, self.app_id)) from e  # noqa
+            raise ServiceUnavailable("Could not delete resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
 
     def log(self, message, level=logging.INFO):
         """Logs a message in the context of this service.
@@ -92,10 +92,10 @@ def log(self, message, level=logging.INFO):
         This prefixes log messages with an application "tag" that the customized
         drycc-logspout will be on the lookout for.  When it's seen, the message-- usually
         an application event of some sort like releasing or scaling, will be considered
-        as "belonging" to the application instead of the controller and will be handled
+        as "belonging" to the application instead of the contoller and will be handled
         accordingly.
         """
-        logger.log(level, "[{}]: {}".format(self.id, message))
+        logger.log(level, "[{}]: {}".format(self.uuid, message))
 
     def bind(self, *args, **kwargs):
         if self.status != "Ready":
@@ -124,12 +124,14 @@ def bind(self, *args, **kwargs):
                 raise ServiceUnavailable(msg) from e
 
     def unbind(self, *args, **kwargs):
-        if self.binding != "Ready":
+        if not self.binding:
             raise DryccException("the resource is not binding")
         try:
             # We raise an exception when a resource doesn't exist
             self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
             self._scheduler.servicecatalog.delete_binding(self.app.id, self.name)
+            self.binding = None
+            self.save()
         except KubeException as e:
             raise ServiceUnavailable("Could not unbind resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
 
@@ -138,10 +140,7 @@ def attach_update(self, *args, **kwargs):
             data = self._scheduler.servicecatalog.get_instance(
                 self.app.id, self.name).json()
         except KubeException as e:
-            self.log("certificate {} does not exist".format(self.app.id),
-                     level=logging.INFO)
-            data = None
-            logger.info(e)
+            self.DryccException("resource {} does not exist".format(self.name))
         try:
             version = data["metadata"]["resourceVersion"]
             instance = self.plan.split(":")
@@ -149,10 +148,14 @@ def attach_update(self, *args, **kwargs):
                 "instance_class": instance[0],
                 "instance_plan": ":".join(instance[1:]),
                 "parameters": self.options,
+                "external_id": data["spec"]["externalID"]
             }
             self._scheduler.servicecatalog.put_instance(
                 self.app.id, self.name, version, **kwargs
             )
+            # self._scheduler.servicecatalog.patch_instance(
+            #     self.app.id, self.name, version, **kwargs
+            # )
             # create/patch/put  retrieve_task
             data = {
                 "task_id": uuid.uuid4().hex,
@@ -171,7 +174,7 @@ def retrieve(self, *args, **kwargs):
                 resp_i = self._scheduler.servicecatalog.get_instance(
                     self.app.id, self.name).json()
                 self.status = resp_i.get('status', {}).\
-                    get('lastConditionState', '').lower()
+                    get('lastConditionState')
                 update_flag = True
             except KubeException as e:
                 logger.info("retrieve instance info error: {}".format(e))
@@ -181,7 +184,7 @@ def retrieve(self, *args, **kwargs):
                 resp_b = self._scheduler.servicecatalog.get_binding(
                     self.app.id, self.name).json()
                 self.binding = resp_b.get('status', {}).\
-                    get('lastConditionState', '').lower()
+                    get('lastConditionState')
                 self.options = resp_b.get('spec', {}).get('parameters', {})
                 update_flag = True
                 secret_name = resp_b.get('spec', {}).get('secretName')
@@ -213,12 +216,8 @@ def detach_resource(self, *args, **kwargs):
                 logger.info("delete binding info error: {}".format(e))
             self.binding = None
 
-        if (self.status != "Ready") or (self.binding is None):
-            try:
-                self._scheduler.servicecatalog.delete_instance(
-                    self.app.id, self.name)
-            except KubeException as e:
-                logger.info("retrieve instance info error: {}".format(e))
+        if (self.status != "Ready") or (not self.binding):
+            self.delete()
 
 
 @task
@@ -234,7 +233,7 @@ def retrieve_task(data):
         if _:
             return True
         else:
-            t = time.time() - resource.created.timestamps()
+            t = time.time() - resource.created.timestamp()
             if t < 3600:
                 apply_async(retrieve_task, delay=30000, args=(data, ))
             elif t < 3600 * 12:
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 4b1c55b2d..e68d3bcbf 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -659,6 +659,7 @@ class ResourceSerializer(serializers.ModelSerializer):
     owner = serializers.ReadOnlyField(source='owner.username')
     name = serializers.CharField(max_length=63, required=True)
     plan = serializers.CharField(max_length=128, required=True)
+    data = JSONFieldSerializer(required=False, binary=True)
     options = JSONFieldSerializer(required=False, binary=True)
 
     class Meta:
@@ -670,8 +671,7 @@ def update(self, instance, validated_data):
         if instance.plan.split(':')[0] != validated_data.get('plan', '').split(':')[0]:  # noqa
             raise DryccException("the resource cann't changed")
         instance.plan = validated_data.get('plan')
-        if validated_data.get('options'):
-            instance.options = validated_data.get('options')
+        instance.options = validated_data.get('options')
         instance.attach_update()
         instance.save()
         return instance
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index d49395314..1a987cdca 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -392,10 +392,10 @@
     'default': {
         'ENGINE': 'django.db.backends.postgresql',
         'NAME': os.environ.get(
-            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc')),
-        'USER': os.environ.get('DRYCC_DATABASE_USER', ''),
-        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', ''),
-        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', ''),
+            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller')),
+        'USER': os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller'),
+        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', '123456'),
+        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', '192.168.6.50'),
         'PORT': os.environ.get('DRYCC_DATABASE_SERVICE_PORT', 5432),
         # https://docs.djangoproject.com/en/1.11/ref/databases/#persistent-connections
         'CONN_MAX_AGE': 600,
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
index 89e974124..afa6f9eb9 100644
--- a/rootfs/api/views.py
+++ b/rootfs/api/views.py
@@ -792,10 +792,14 @@ def binding(self, request, *args, **kwargs):
         bind_action = self.request.data.get('bind_action', '').lower()
         if bind_action == 'bind':
             resource.bind()
-            return HttpResponse(status=status.HTTP_202_ACCEPTED)
+            serializer = self.get_serializer(resource, many=False)
+            logger.info("resoruce bind response data: {}".format(serializer))
+            return Response(serializer.data)
         elif bind_action == 'unbind':
             resource.unbind()
-            return HttpResponse(status=status.HTTP_202_ACCEPTED)
+            serializer = self.get_serializer(resource, many=False)
+            logger.info("resoruce unbind response data: {}".format(serializer))
+            return Response(serializer.data)
         else:
             return Http404("unknown action")
 
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index dd50ab8b1..fb8530ded 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -15,6 +15,7 @@ morph==0.1.4
 ndg-httpsclient==0.5.1
 packaging==20.4
 pyasn1==0.4.8
+tornado==5.1.1
 pynsq==0.9.0
 psycopg2-binary==2.8.5
 pyldap==3.0.0.post1
diff --git a/rootfs/scheduler/__init__.py b/rootfs/scheduler/__init__.py
index 952770df6..4de862b87 100644
--- a/rootfs/scheduler/__init__.py
+++ b/rootfs/scheduler/__init__.py
@@ -221,6 +221,11 @@ def http_patch(self, path, data=None, **kwargs):
         """
         try:
             url = urljoin(self.url, path)
+            # accepted media types include:
+            # application/json-patch+json,
+            # application/merge-patch+json,
+            # application/apply-patch+yaml
+            # self.session.headers["Content-Type"] = "application/json-patch+json"
             response = self.session.patch(url, data=data, **kwargs)
         except requests.exceptions.ConnectionError as err:
             # reraise as KubeException, but log stacktrace.
diff --git a/rootfs/scheduler/resources/servicecatalog.py b/rootfs/scheduler/resources/servicecatalog.py
index 3d5d8ee44..14d5ef033 100644
--- a/rootfs/scheduler/resources/servicecatalog.py
+++ b/rootfs/scheduler/resources/servicecatalog.py
@@ -1,6 +1,9 @@
+import logging
 from scheduler.resources import Resource
 from scheduler.exceptions import KubeHTTPException
 
+logger = logging.getLogger(__name__)
+
 
 class ServiceCatalog(Resource):
     api_version = 'servicecatalog.k8s.io/v1beta1'
@@ -22,14 +25,29 @@ def service_instance_manifest(namespace, name, version=None, **kwargs):
             "spec": {
                 "clusterServiceClassExternalName": kwargs.get('instance_class'),
                 "clusterServicePlanExternalName": kwargs.get('instance_plan'),
-                #{"param - 1": "value - 1","param - 2": "value - 2"}  # noqa
-                "parameters": kwargs.get('parameters'),
             }
         }
         if version:
             data["metadata"]["resourceVersion"] = version
+        if kwargs.get('parameters'):
+            data["spec"]["parameters"] = kwargs.get('parameters')
+        if kwargs.get('external_id'):
+            data["spec"]["externalID"] = kwargs.get('external_id')
         return data
 
+    # @staticmethod
+    # def service_instance_patch_manifest(namespace, name, version=None, **kwargs):
+    #     data = {
+    #         "spec": {
+    #             "clusterServicePlanExternalName": kwargs.get('instance_plan'),
+    #             "parameters": kwargs.get('parameters'),
+    #         }
+    #     }
+    #     if version:
+    #         data["metadata"] = {}
+    #         data["metadata"]["resourceVersion"] = version
+    #     return data
+
     def get_instance(self, namespace, name=None):
         """
         Fetch a single serviceinstance or a list of serviceinstances
@@ -52,6 +70,7 @@ def create_instance(self, namespace, name, **kwargs):
         """
         url = self.api('/namespaces/{}/serviceinstances', namespace)
         data = self.service_instance_manifest(namespace, name, **kwargs)
+        logging.info("create_instance_data:{}".format(data))
         response = self.http_post(url, json=data)
         if not response.status_code == 201:
             raise KubeHTTPException(
@@ -65,6 +84,7 @@ def put_instance(self, namespace, name, version, **kwargs):
         """
         url = self.api('/namespaces/{}/serviceinstances/{}', namespace, name)
         data = self.service_instance_manifest(namespace, name, version, **kwargs)
+        logger.info("put_instance data:{}".format(data))
         response = self.http_put(url, json=data)
         if not response.status_code == 200:
             raise KubeHTTPException(
@@ -72,6 +92,16 @@ def put_instance(self, namespace, name, version, **kwargs):
                 "update serviceinstances {}".format(namespace))
         return response
 
+    # def patch_instance(self, namespace, name, version, **kwargs):
+    #     data = self.service_instance_patch_manifest(
+    #         namespace, name, version, **kwargs)
+    #     url = self.api("/namespaces/{}/serviceinstances/{}", namespace, name)
+    #     logger.info("patch_instance data:{}".format(data))
+    #     response = self.http_patch(url, json=data)
+    #     if self.unhealthy(response.status_code):
+    #         raise KubeHTTPException(response, "patch serviceinstances {}".format(namespace))
+    #     return response
+
     def delete_instance(self, namespace, name):
         """
         Delete serviceinstances

From 4f013ef07828b15da27fe95c1125f907744d18b1 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 10 Sep 2020 15:52:49 +0800
Subject: [PATCH 06/13] chore(wsgi): add tornado 6 support

---
 rootfs/bin/boot               | 2 +-
 rootfs/drycc/gunicorn/wsgi.py | 7 +++++++
 rootfs/drycc/wsgi.py          | 7 +++++++
 rootfs/requirements.txt       | 1 -
 4 files changed, 15 insertions(+), 2 deletions(-)
 create mode 100644 rootfs/drycc/gunicorn/wsgi.py
 create mode 100644 rootfs/drycc/wsgi.py

diff --git a/rootfs/bin/boot b/rootfs/bin/boot
index ecc35a34f..9012b07d5 100755
--- a/rootfs/bin/boot
+++ b/rootfs/bin/boot
@@ -30,7 +30,7 @@ python /app/manage.py migrate --noinput
 # spawn a gunicorn server in the background
 echo ""
 echo "Starting up Gunicorn"
-gunicorn -c /app/drycc/gunicorn/config.py api.wsgi &
+gunicorn -c /app/drycc/gunicorn/config.py drycc.gunicorn.wsgi &
 
 echo ""
 echo "Loading database information to Kubernetes in the background"
diff --git a/rootfs/drycc/gunicorn/wsgi.py b/rootfs/drycc/gunicorn/wsgi.py
new file mode 100644
index 000000000..dbf67260a
--- /dev/null
+++ b/rootfs/drycc/gunicorn/wsgi.py
@@ -0,0 +1,7 @@
+from tornado.wsgi import WSGIContainer
+
+
+from api.wsgi import application as handler
+
+
+application = WSGIContainer(handler)
diff --git a/rootfs/drycc/wsgi.py b/rootfs/drycc/wsgi.py
new file mode 100644
index 000000000..dbf67260a
--- /dev/null
+++ b/rootfs/drycc/wsgi.py
@@ -0,0 +1,7 @@
+from tornado.wsgi import WSGIContainer
+
+
+from api.wsgi import application as handler
+
+
+application = WSGIContainer(handler)
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index fb8530ded..dd50ab8b1 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -15,7 +15,6 @@ morph==0.1.4
 ndg-httpsclient==0.5.1
 packaging==20.4
 pyasn1==0.4.8
-tornado==5.1.1
 pynsq==0.9.0
 psycopg2-binary==2.8.5
 pyldap==3.0.0.post1

From 92c4712642be3dd77bc11ff4f5104a468e6ec27f Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Sat, 12 Sep 2020 02:25:11 +0800
Subject: [PATCH 07/13] chore(utils): use threads replace asyncio

---
 rootfs/api/models/app.py |  8 ++++----
 rootfs/api/utils.py      | 28 +++-------------------------
 2 files changed, 7 insertions(+), 29 deletions(-)

diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py
index 422764454..7ed652b22 100644
--- a/rootfs/api/models/app.py
+++ b/rootfs/api/models/app.py
@@ -26,7 +26,7 @@
 from api.models.tls import TLS
 from api.models.appsettings import AppSettings
 from api.models.volume import Volume
-from api.utils import generate_app_name, async_run
+from api.utils import generate_app_name, apply_tasks
 from scheduler import KubeHTTPException, KubeException
 
 logger = logging.getLogger(__name__)
@@ -383,7 +383,7 @@ def restart(self, **kwargs):  # noqa
                 ) for pod in self.list_pods(**kwargs)
             ]
 
-            async_run(tasks)
+            apply_tasks(tasks)
         except Exception as e:
             err = "warning, some pods failed to stop:\n{}".format(str(e))
             self.log(err, logging.WARNING)
@@ -528,7 +528,7 @@ def _scale_pods(self, scale_types):
             # create the application config in k8s (secret in this case) for all deploy objects
             self.set_application_config(release)
 
-            async_run(tasks)
+            apply_tasks(tasks)
         except Exception as e:
             err = '(scale): {}'.format(e)
             self.log(err, logging.ERROR)
@@ -619,7 +619,7 @@ def deploy(self, release, force_deploy=False, rollback_on_failure=True):  # noqa
             ]
 
             try:
-                async_run(tasks)
+                apply_tasks(tasks)
             except KubeException as e:
                 # Don't rollback if the previous release doesn't have a build which means
                 # this is the first build and all the previous releases are just config changes.
diff --git a/rootfs/api/utils.py b/rootfs/api/utils.py
index 3a1bc3647..93da3a714 100644
--- a/rootfs/api/utils.py
+++ b/rootfs/api/utils.py
@@ -1,7 +1,6 @@
 """
 Helper functions used by the Drycc server.
 """
-import asyncio
 import base64
 import concurrent
 import hashlib
@@ -145,7 +144,7 @@ def dict_merge(origin, merge):
     return result
 
 
-def async_run(tasks):
+def apply_tasks(tasks):
     """
     run a group of tasks async
     Requires the tasks arg to be a list of functools.partial()
@@ -153,35 +152,14 @@ def async_run(tasks):
     if not tasks:
         return
 
-    # start a new async event loop
-    loop = asyncio.get_event_loop()
-    # https://github.com/python/asyncio/issues/258
     executor = concurrent.futures.ThreadPoolExecutor(5)
-    loop.set_default_executor(executor)
-
-    async_tasks = [asyncio.ensure_future(async_task(task, loop)) for task in tasks]
-    # run tasks in parallel
-    loop.run_until_complete(asyncio.wait(async_tasks))
-    # deal with errors (exceptions, etc)
-    for task in async_tasks:
-        error = task.exception()
+    for future in [executor.submit(task) for task in tasks]:
+        error = future.exception()
         if error is not None:
             raise error
-
     executor.shutdown(wait=True)
 
 
-async def async_task(params, loop):
-    """
-    Perform a task asynchronously.
-    """
-    # get the calling function
-    logger.debug('Running {}'.format(params))
-    # This executes a task in its own thread (in parallel)
-    await loop.run_in_executor(None, params)
-    logger.debug('Finished running {}'.format(params))
-
-
 if __name__ == "__main__":
     import doctest
     doctest.testmod()

From de95f543c8a1284cb558b70bde049d35be0203e6 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Sat, 12 Sep 2020 22:58:04 +0800
Subject: [PATCH 08/13] chore(ldap): add AUTH_LDAP_USER_FLAGS_BY_GROUP

---
 rootfs/api/settings/production.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 1a987cdca..97b7c7c2d 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -412,6 +412,9 @@
 LDAP_USER_FILTER = os.environ.get('LDAP_USER_FILTER', 'username')
 LDAP_GROUP_BASEDN = os.environ.get('LDAP_GROUP_BASEDN', '')
 LDAP_GROUP_FILTER = os.environ.get('LDAP_GROUP_FILTER', '')
+LDAP_ACTIVE_GROUP = os.environ.get('LDAP_ACTIVE_GROUP', '')
+LDAP_STAFF_GROUP = os.environ.get('LDAP_STAFF_GROUP', '')
+LDAP_SUPERUSER_GROUP = os.environ.get('LDAP_SUPERUSER_GROUP', '')
 
 # Django LDAP backend configuration.
 # See https://pythonhosted.org/django-auth-ldap/reference.html
@@ -435,6 +438,11 @@
         scope=ldap.SCOPE_SUBTREE,
         filterstr="(%s)" % LDAP_GROUP_FILTER
     )
+    AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+        'is_active': LDAP_ACTIVE_GROUP,
+        'is_staff': LDAP_STAFF_GROUP,
+        'is_superuser': LDAP_SUPERUSER_GROUP,
+    }
     AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
     AUTH_LDAP_USER_ATTR_MAP = {
         "first_name": "givenName",

From 720ebe376804ca12d01a513d08f09a2bdb97d2f5 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Sat, 12 Sep 2020 23:38:32 +0800
Subject: [PATCH 09/13] chore(charts): add custom controller environment
 variables support

---
 charts/controller/templates/controller-deployment.yaml | 6 ++++--
 charts/controller/values.yaml                          | 6 ++++++
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/charts/controller/templates/controller-deployment.yaml b/charts/controller/templates/controller-deployment.yaml
index d706b6b93..cb5b47894 100644
--- a/charts/controller/templates/controller-deployment.yaml
+++ b/charts/controller/templates/controller-deployment.yaml
@@ -128,14 +128,16 @@ spec:
               secretKeyRef:
                 name: database-creds
                 key: password
-          - name: RESERVED_NAMES
-            value: "drycc, drycc-builder, drycc-monitor-grafana"
           - name: WORKFLOW_NAMESPACE
             valueFrom:
               fieldRef:
                 fieldPath: metadata.namespace
           - name: DRYCC_NSQD_ADDRS
             value: "{{range $i := until $nsqdNodeCount}}drycc-nsqd-{{$i}}.drycc-nsqd.{{ $.Release.Namespace }}.svc.cluster.local:{{$.Values.nsqd.tcp_port}}{{if lt (add 1 $i) $nsqdNodeCount}},{{end}}{{end}}"
+          {{- range $key, $value := .Values.environment }}
+          - name: {{ $key }}
+            value: {{ $value | quote }}
+          {{- end }}
           volumeMounts:
             - mountPath: /etc/slugrunner
               name: slugrunner-config
diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml
index d342f7ef1..3701a6cb6 100644
--- a/charts/controller/values.yaml
+++ b/charts/controller/values.yaml
@@ -19,6 +19,12 @@ k8s_api_verify_tls: "true"
 # Set storageClassName, It is used for application mount.
 app_storage_class: ""
 
+# Any custom controller environment variables
+# can be specified as key-value pairs under environment
+# this is usually a non required setting.
+environment:
+  RESERVED_NAMES: "drycc, drycc-builder, drycc-monitor-grafana"
+
 nsqd:
   replicas: 1
   tcp_port: 4150

From 53d36ba1c72315963cdd55ce3d34bfe8d4913d94 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Mon, 14 Sep 2020 13:26:24 +0800
Subject: [PATCH 10/13] feat(controller):add drycc resource cmd

---
 rootfs/api/models/resource.py     |  7 ++++---
 rootfs/api/settings/production.py |  8 ++++----
 rootfs/api/tests/test_resource.py | 29 +++++++++++++++--------------
 3 files changed, 23 insertions(+), 21 deletions(-)

diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index d4aba2bb9..cff5439bc 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -92,7 +92,7 @@ def log(self, message, level=logging.INFO):
         This prefixes log messages with an application "tag" that the customized
         drycc-logspout will be on the lookout for.  When it's seen, the message-- usually
         an application event of some sort like releasing or scaling, will be considered
-        as "belonging" to the application instead of the contoller and will be handled
+        as "belonging" to the application instead of the controller and will be handled
         accordingly.
         """
         logger.log(level, "[{}]: {}".format(self.uuid, message))
@@ -131,6 +131,7 @@ def unbind(self, *args, **kwargs):
             self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
             self._scheduler.servicecatalog.delete_binding(self.app.id, self.name)
             self.binding = None
+            self.data = {}
             self.save()
         except KubeException as e:
             raise ServiceUnavailable("Could not unbind resource {} for application {}".format(self.name, self.app_id)) from e  # noqa
@@ -175,6 +176,7 @@ def retrieve(self, *args, **kwargs):
                     self.app.id, self.name).json()
                 self.status = resp_i.get('status', {}).\
                     get('lastConditionState')
+                self.options = resp_i.get('spec', {}).get('parameters', {})
                 update_flag = True
             except KubeException as e:
                 logger.info("retrieve instance info error: {}".format(e))
@@ -185,7 +187,6 @@ def retrieve(self, *args, **kwargs):
                     self.app.id, self.name).json()
                 self.binding = resp_b.get('status', {}).\
                     get('lastConditionState')
-                self.options = resp_b.get('spec', {}).get('parameters', {})
                 update_flag = True
                 secret_name = resp_b.get('spec', {}).get('secretName')
                 if secret_name:
@@ -197,7 +198,7 @@ def retrieve(self, *args, **kwargs):
                 logger.info("retrieve binding info error: {}".format(e))
         if update_flag is True:
             self.save()
-        if self.status and self.binding:
+        if self.status == "Ready" and self.binding == "Ready":
             return True
         else:
             return False
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 97b7c7c2d..a6f3054d9 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -392,10 +392,10 @@
     'default': {
         'ENGINE': 'django.db.backends.postgresql',
         'NAME': os.environ.get(
-            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller')),
-        'USER': os.environ.get('DRYCC_DATABASE_USER', 'drycc_controller'),
-        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', '123456'),
-        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', '192.168.6.50'),
+            'DRYCC_DATABASE_NAME', os.environ.get('DRYCC_DATABASE_USER', 'drycc')),
+        'USER': os.environ.get('DRYCC_DATABASE_USER', ''),
+        'PASSWORD': os.environ.get('DRYCC_DATABASE_PASSWORD', ''),
+        'HOST': os.environ.get('DRYCC_DATABASE_SERVICE_HOST', ''),
         'PORT': os.environ.get('DRYCC_DATABASE_SERVICE_PORT', 5432),
         # https://docs.djangoproject.com/en/1.11/ref/databases/#persistent-connections
         'CONN_MAX_AGE': 600,
diff --git a/rootfs/api/tests/test_resource.py b/rootfs/api/tests/test_resource.py
index 554c09d84..f41ed7173 100644
--- a/rootfs/api/tests/test_resource.py
+++ b/rootfs/api/tests/test_resource.py
@@ -102,21 +102,22 @@ def test_resource_delete(self, mock_requests):
         response = self.client.delete(url)
         self.assertEqual(response.status_code, 204)
 
-    def test_resource_update(self, mock_requests):
-        """
-        Test that resource is delete from a app
-        """
-        # create
-        app_id = self.create_app()
-        data = {'name': 'mysql', 'plan': 'mysql:5.6'}
-        self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
 
-        # Update
-        url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
-                                                          name='mysql')
-        data = {'plan': 'mysql:5.7'}
-        response = self.client.put(url, data=data)
-        self.assertEqual(response.status_code, 200)
+    # def test_resource_update(self, mock_requests):
+    #     """
+    #     Test that update resource from a app, Associated with apply_async
+    #     """
+    #     # create
+    #     app_id = self.create_app()
+    #     data = {'name': 'mysql', 'plan': 'mysql:5.6'}
+    #     self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
+    #
+    #     # Update
+    #     url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
+    #                                                       name='mysql')
+    #     data = {'plan': 'mysql:5.7'}
+    #     response = self.client.put(url, data=data)
+    #     self.assertEqual(response.status_code, 200)
 
     def test_resource_bind(self, mock_requests):
         # create

From c740764230b3154882f1fcca43700ba152236b8d Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Mon, 14 Sep 2020 16:36:07 +0800
Subject: [PATCH 11/13] chore(controller):remove resources code which is
 commented out

---
 rootfs/api/models/resource.py                 |  6 -----
 rootfs/api/tests/test_resource.py             | 17 --------------
 rootfs/scheduler/resources/servicecatalog.py  | 23 -------------------
 rootfs/scheduler/tests/test_servicecatalog.py |  7 ------
 4 files changed, 53 deletions(-)

diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index cff5439bc..964f13f5d 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -58,7 +58,6 @@ def attach(self, *args, **kwargs):
                 self._scheduler.servicecatalog.create_instance(
                     self.app.id, self.name, **kwargs
                 )
-                # create/patch/put  retrieve_task
                 data = {
                     "task_id": uuid.uuid4().hex,
                     "resource_id": str(self.uuid),
@@ -112,7 +111,6 @@ def bind(self, *args, **kwargs):
             try:
                 self._scheduler.servicecatalog.create_binding(
                     self.app.id, self.name, **kwargs)
-                # create/patch/put  retrieve_task
                 data = {
                     "task_id": uuid.uuid4().hex,
                     "resource_id": str(self.uuid),
@@ -154,10 +152,6 @@ def attach_update(self, *args, **kwargs):
             self._scheduler.servicecatalog.put_instance(
                 self.app.id, self.name, version, **kwargs
             )
-            # self._scheduler.servicecatalog.patch_instance(
-            #     self.app.id, self.name, version, **kwargs
-            # )
-            # create/patch/put  retrieve_task
             data = {
                 "task_id": uuid.uuid4().hex,
                 "resource_id": str(self.uuid),
diff --git a/rootfs/api/tests/test_resource.py b/rootfs/api/tests/test_resource.py
index f41ed7173..b39d036b4 100644
--- a/rootfs/api/tests/test_resource.py
+++ b/rootfs/api/tests/test_resource.py
@@ -102,23 +102,6 @@ def test_resource_delete(self, mock_requests):
         response = self.client.delete(url)
         self.assertEqual(response.status_code, 204)
 
-
-    # def test_resource_update(self, mock_requests):
-    #     """
-    #     Test that update resource from a app, Associated with apply_async
-    #     """
-    #     # create
-    #     app_id = self.create_app()
-    #     data = {'name': 'mysql', 'plan': 'mysql:5.6'}
-    #     self.client.post('/v2/apps/{}/resources'.format(app_id), data=data)
-    #
-    #     # Update
-    #     url = '/v2/apps/{app_id}/resources/{name}'.format(app_id=app_id,
-    #                                                       name='mysql')
-    #     data = {'plan': 'mysql:5.7'}
-    #     response = self.client.put(url, data=data)
-    #     self.assertEqual(response.status_code, 200)
-
     def test_resource_bind(self, mock_requests):
         # create
         app_id = self.create_app()
diff --git a/rootfs/scheduler/resources/servicecatalog.py b/rootfs/scheduler/resources/servicecatalog.py
index 14d5ef033..8ee78c9db 100644
--- a/rootfs/scheduler/resources/servicecatalog.py
+++ b/rootfs/scheduler/resources/servicecatalog.py
@@ -35,19 +35,6 @@ def service_instance_manifest(namespace, name, version=None, **kwargs):
             data["spec"]["externalID"] = kwargs.get('external_id')
         return data
 
-    # @staticmethod
-    # def service_instance_patch_manifest(namespace, name, version=None, **kwargs):
-    #     data = {
-    #         "spec": {
-    #             "clusterServicePlanExternalName": kwargs.get('instance_plan'),
-    #             "parameters": kwargs.get('parameters'),
-    #         }
-    #     }
-    #     if version:
-    #         data["metadata"] = {}
-    #         data["metadata"]["resourceVersion"] = version
-    #     return data
-
     def get_instance(self, namespace, name=None):
         """
         Fetch a single serviceinstance or a list of serviceinstances
@@ -92,16 +79,6 @@ def put_instance(self, namespace, name, version, **kwargs):
                 "update serviceinstances {}".format(namespace))
         return response
 
-    # def patch_instance(self, namespace, name, version, **kwargs):
-    #     data = self.service_instance_patch_manifest(
-    #         namespace, name, version, **kwargs)
-    #     url = self.api("/namespaces/{}/serviceinstances/{}", namespace, name)
-    #     logger.info("patch_instance data:{}".format(data))
-    #     response = self.http_patch(url, json=data)
-    #     if self.unhealthy(response.status_code):
-    #         raise KubeHTTPException(response, "patch serviceinstances {}".format(namespace))
-    #     return response
-
     def delete_instance(self, namespace, name):
         """
         Delete serviceinstances
diff --git a/rootfs/scheduler/tests/test_servicecatalog.py b/rootfs/scheduler/tests/test_servicecatalog.py
index f8e9fcdcd..5f779e14c 100644
--- a/rootfs/scheduler/tests/test_servicecatalog.py
+++ b/rootfs/scheduler/tests/test_servicecatalog.py
@@ -101,13 +101,6 @@ def test_create_binding(self):
         self.scheduler.ns.create("test-serviceinstance")
         self.create_binding(namespace="test-serviceinstance")
 
-    # def test_create_binding_failure(self):
-    #     with self.assertRaises(
-    #         KubeHTTPException,
-    #         msg='failed to create servicebindings doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa
-    #     ):
-    #         self.create_binding(self.namespace, 'doesnotexist')
-
     def test_binding_delete(self):
         # test success
         name = self.create_binding(namespace="test-serviceinstance")

From b22b6fd4fb9581899911b5f14a6cd78d982ee7d9 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@qq.com>
Date: Sat, 19 Sep 2020 21:43:38 +0800
Subject: [PATCH 12/13] chore(volumes): mount path check container_types

---
 rootfs/api/views.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/rootfs/api/views.py b/rootfs/api/views.py
index afa6f9eb9..a1d5e9c9c 100644
--- a/rootfs/api/views.py
+++ b/rootfs/api/views.py
@@ -665,7 +665,8 @@ def path(self, request, *args, **kwargs):
             raise DryccException("path is a required field")
         obj = self.get_object()
         container_types = [_ for _ in new_path.keys()
-                           if _ not in obj.app.procfile_structure.keys()]
+                           if _ not in obj.app.procfile_structure.keys() or
+                           _ not in obj.app.structure.keys()]
         if container_types:
             raise DryccException("process type {} is not included in profile".
                                  format(','.join(container_types)))

From 6328b3f0d646f0aa2b7fa8be1f30694840381581 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@qq.com>
Date: Sun, 20 Sep 2020 21:01:32 +0800
Subject: [PATCH 13/13] chore(resourcess): change status\binding model type

---
 rootfs/api/migrations/0001_initial.py | 4 ++--
 rootfs/api/models/resource.py         | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/rootfs/api/migrations/0001_initial.py b/rootfs/api/migrations/0001_initial.py
index 9dd73ce60..53dffa2d3 100644
--- a/rootfs/api/migrations/0001_initial.py
+++ b/rootfs/api/migrations/0001_initial.py
@@ -260,8 +260,8 @@ class Migration(migrations.Migration):
                 ('name', models.CharField(max_length=63, validators=[api.models.validate_label])),
                 ('plan', models.CharField(max_length=128)),
                 ('data', jsonfield.fields.JSONField(blank=True, default={})),
-                ('status', models.CharField(max_length=32, null=True)),
-                ('binding', models.CharField(max_length=32, null=True)),
+                ('status', models.TextField(blank=True, null=True)),
+                ('binding', models.TextField(blank=True, null=True)),
                 ('options', jsonfield.fields.JSONField(blank=True, default={})),
                 ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
                 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
diff --git a/rootfs/api/models/resource.py b/rootfs/api/models/resource.py
index 964f13f5d..e19889459 100644
--- a/rootfs/api/models/resource.py
+++ b/rootfs/api/models/resource.py
@@ -20,8 +20,8 @@ class Resource(UuidAuditedModel):
     name = models.CharField(max_length=63, validators=[validate_label])
     plan = models.CharField(max_length=128)
     data = JSONField(default={}, blank=True)
-    status = models.CharField(max_length=32, null=True)
-    binding = models.CharField(max_length=32, null=True)
+    status = models.TextField(blank=True, null=True)
+    binding = models.TextField(blank=True, null=True)
     options = JSONField(default={}, blank=True)
 
     class Meta:
