diff --git a/charts/controller/templates/_helpers.tpl b/charts/controller/templates/_helpers.tpl
index ef97d9513..d137cb15e 100644
--- a/charts/controller/templates/_helpers.tpl
+++ b/charts/controller/templates/_helpers.tpl
@@ -97,12 +97,12 @@ env:
 - name: DRYCC_TS_USER
   valueFrom:
     secretKeyRef:
-      name: database-creds
+      name: timeseries-creds
       key: user
 - name: DRYCC_TS_PASSWORD
   valueFrom:
     secretKeyRef:
-      name: database-creds
+      name: timeseries-creds
       key: password
 - name: DRYCC_DATABASE_MONITOR_URL
   value: "postgres://$(DRYCC_TS_USER):$(DRYCC_TS_PASSWORD)@drycc-timeseries-replica.{{.Release.Namespace}}.svc.{{.Values.global.clusterDomain}}:5432/monitor"
diff --git a/charts/controller/templates/controller-clusterrole.yaml b/charts/controller/templates/controller-clusterrole.yaml
index 153793793..d16752740 100644
--- a/charts/controller/templates/controller-clusterrole.yaml
+++ b/charts/controller/templates/controller-clusterrole.yaml
@@ -69,7 +69,7 @@ rules:
   verbs: ["get"]
 - apiGroups: ["apps"]
   resources: ["replicasets"]
-  verbs: ["get", "list", "watch"]
+  verbs: ["get", "list", "watch", "delete"]
 - apiGroups: [""]
   resources: ["persistentvolumeclaims"]
   verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
diff --git a/rootfs/api/apps_extra/social_core/actions.py b/rootfs/api/apps_extra/social_core/actions.py
index 2224af6db..379ce539b 100644
--- a/rootfs/api/apps_extra/social_core/actions.py
+++ b/rootfs/api/apps_extra/social_core/actions.py
@@ -1,46 +1,51 @@
+import json
 from urllib.parse import quote
 
-from social_core.utils import sanitize_redirect, user_is_authenticated, \
-                   user_is_active, partial_pipeline_data, setting_url
+from social_core.utils import (
+    partial_pipeline_data,
+    sanitize_redirect,
+    setting_url,
+    user_is_active,
+    user_is_authenticated,
+)
 
 
-def do_auth(backend, redirect_name='next'):
+def do_auth(backend, redirect_name="next"):
     # Save any defined next value into session
     data = backend.strategy.request_data(merge=False)
 
     # Save extra data into session.
-    for field_name in backend.setting('FIELDS_STORED_IN_SESSION', []):
+    for field_name in backend.setting("FIELDS_STORED_IN_SESSION", []):
         if field_name in data:
             backend.strategy.session_set(field_name, data[field_name])
         else:
             backend.strategy.session_set(field_name, None)
-    # uri = None
+
     if redirect_name in data:
         # Check and sanitize a user-defined GET/POST next field value
         redirect_uri = data[redirect_name]
-        if backend.setting('SANITIZE_REDIRECTS', True):
-            allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \
-                            [backend.strategy.request_host()]
+        if backend.setting("SANITIZE_REDIRECTS", True):
+            allowed_hosts = backend.setting("ALLOWED_REDIRECT_HOSTS", []) + [
+                backend.strategy.request_host()
+            ]
             redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri)
         backend.strategy.session_set(
-            redirect_name,
-            redirect_uri or backend.setting('LOGIN_REDIRECT_URL')
+            redirect_name, redirect_uri or backend.setting("LOGIN_REDIRECT_URL")
         )
     response = backend.start()
-    url = response.url.split('?')[1]
+    url = response.url.split("?")[1]
 
     def form2json(form_data):
         from urllib.parse import parse_qs, urlparse
-        query = urlparse('?' + form_data).query
+        query = urlparse("?" + form_data).query
         params = parse_qs(query)
         return {key: params[key][0] for key in params}
     from django.core.cache import cache
-    cache.set("oidc_key_" + data.get('key', ''), form2json(url).get('state'), 60 * 10)
+    cache.set("oidc_key_" + data.get("key", ""), form2json(url).get("state"), 60 * 10)
     return response
 
 
-def do_complete(backend, login, user=None, redirect_name='next',
-                *args, **kwargs):
+def do_complete(backend, login, user=None, redirect_name="next", *args, **kwargs):
     data = backend.strategy.request_data()
 
     is_authenticated = user_is_authenticated(user)
@@ -56,8 +61,9 @@ def do_complete(backend, login, user=None, redirect_name='next',
 
     # pop redirect value before the session is trashed on login(), but after
     # the pipeline so that the pipeline can change the redirect if needed
-    redirect_value = backend.strategy.session_get(redirect_name, '') or \
-        data.get(redirect_name, '')
+    redirect_value = backend.strategy.session_get(redirect_name, "") or data.get(
+        redirect_name, ""
+    )
 
     # check if the output value is something else than a user and just
     # return it to the client
@@ -67,54 +73,99 @@ def do_complete(backend, login, user=None, redirect_name='next',
 
     if is_authenticated:
         if not user:
-            url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL')
+            url = setting_url(backend, redirect_value, "LOGIN_REDIRECT_URL")
         else:
-            url = setting_url(backend, redirect_value,
-                              'NEW_ASSOCIATION_REDIRECT_URL',
-                              'LOGIN_REDIRECT_URL')
+            url = setting_url(
+                backend,
+                redirect_value,
+                "NEW_ASSOCIATION_REDIRECT_URL",
+                "LOGIN_REDIRECT_URL",
+            )
     elif user:
         if user_is_active(user):
             # catch is_new/social_user in case login() resets the instance
-            is_new = getattr(user, 'is_new', False)
+            is_new = getattr(user, "is_new", False)
             social_user = user.social_user
             login(backend, user, social_user)
             # store last login backend name in session
-            backend.strategy.session_set('social_auth_last_login_backend',
-                                         social_user.provider)
+            backend.strategy.session_set(
+                "social_auth_last_login_backend", social_user.provider
+            )
 
             if is_new:
-                url = setting_url(backend,
-                                  'NEW_USER_REDIRECT_URL',
-                                  redirect_value,
-                                  'LOGIN_REDIRECT_URL')
+                url = setting_url(
+                    backend,
+                    "NEW_USER_REDIRECT_URL",
+                    redirect_value,
+                    "LOGIN_REDIRECT_URL",
+                )
             else:
-                url = setting_url(backend, redirect_value,
-                                  'LOGIN_REDIRECT_URL')
+                url = setting_url(backend, redirect_value, "LOGIN_REDIRECT_URL")
         else:
-            if backend.setting('INACTIVE_USER_LOGIN', False):
+            if backend.setting("INACTIVE_USER_LOGIN", False):
                 social_user = user.social_user
                 login(backend, user, social_user)
-            url = setting_url(backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL',
-                              'LOGIN_URL')
+            url = setting_url(
+                backend, "INACTIVE_USER_URL", "LOGIN_ERROR_URL", "LOGIN_URL"
+            )
     else:
-        url = setting_url(backend, 'LOGIN_ERROR_URL', 'LOGIN_URL')
+        url = setting_url(backend, "LOGIN_ERROR_URL", "LOGIN_URL")
 
     if redirect_value and redirect_value != url:
         redirect_value = quote(redirect_value)
-        url += ('&' if '?' in url else '?') + \
-            '{0}={1}'.format(redirect_name, redirect_value)
-
-    if backend.setting('SANITIZE_REDIRECTS', True):
-        allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \
-                        [backend.strategy.request_host()]
-        url = sanitize_redirect(allowed_hosts, url) or \
-            backend.setting('LOGIN_REDIRECT_URL')
+        url += ("&" if "?" in url else "?") + f"{redirect_name}={redirect_value}"
+
+    if backend.setting("SANITIZE_REDIRECTS", True):
+        allowed_hosts = backend.setting("ALLOWED_REDIRECT_HOSTS", []) + [
+            backend.strategy.request_host()
+        ]
+        url = sanitize_redirect(allowed_hosts, url) or backend.setting(
+            "LOGIN_REDIRECT_URL"
+        )
+
     response = backend.strategy.redirect(url)
-    social_auth = user.social_auth.filter(provider='drycc').\
-        order_by('-modified').last()
+    social_auth = user.social_auth.filter(provider="drycc").\
+        order_by("-modified").last()
+    if social_auth and social_auth.extra_data:
+        extra_data = json.loads(social_auth.extra_data) if \
+            isinstance(social_auth.extra_data, str) else social_auth.extra_data
     from django.core.cache import cache
-    cache.set("oidc_state_" + data.get('state'),
-              {'token': social_auth.extra_data.get('id_token', 'fail'),
-               'username': user.username},
+    cache.set("oidc_state_" + data.get("state"),
+              {"token": extra_data.get("id_token", "fail"),
+               "username": user.username},
               60 * 10)
     return response
+
+
+def do_disconnect(
+    backend, user, association_id=None, redirect_name="next", *args, **kwargs
+):
+    partial = partial_pipeline_data(backend, user, *args, **kwargs)
+    if partial:
+        if association_id and not partial.kwargs.get("association_id"):
+            partial.extend_kwargs({"association_id": association_id})
+        response = backend.disconnect(*partial.args, **partial.kwargs)
+        # clean partial data after usage
+        backend.strategy.clean_partial_pipeline(partial.token)
+    else:
+        response = backend.disconnect(
+            user=user, association_id=association_id, *args, **kwargs
+        )
+
+    if isinstance(response, dict):
+        url = backend.strategy.absolute_uri(
+            backend.strategy.request_data().get(redirect_name, "")
+            or backend.setting("DISCONNECT_REDIRECT_URL")
+            or backend.setting("LOGIN_REDIRECT_URL")
+        )
+        if backend.setting("SANITIZE_REDIRECTS", True):
+            allowed_hosts = backend.setting("ALLOWED_REDIRECT_HOSTS", []) + [
+                backend.strategy.request_host()
+            ]
+            url = (
+                sanitize_redirect(allowed_hosts, url)
+                or backend.setting("DISCONNECT_REDIRECT_URL")
+                or backend.setting("LOGIN_REDIRECT_URL")
+            )
+        response = backend.strategy.redirect(url)
+    return response
diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py
index d165d213c..1f8a32763 100644
--- a/rootfs/api/models/app.py
+++ b/rootfs/api/models/app.py
@@ -401,7 +401,8 @@ def cleanup_old(self):
             if scale_type in app_settings.canaries:
                 names.append(self._get_job_id(scale_type, True))
             names.append(self._get_job_id(scale_type, False))
-        deployments = self._scheduler.deployments.get(self.id).json()["items"]
+        labels = {'heritage': 'drycc'}
+        deployments = self._scheduler.deployments.get(self.id, labels=labels).json()["items"]
         if deployments is not None:
             for deployment in deployments:
                 name = deployment['metadata']['name']
diff --git a/rootfs/api/models/release.py b/rootfs/api/models/release.py
index 775102f0f..b420d9e6f 100644
--- a/rootfs/api/models/release.py
+++ b/rootfs/api/models/release.py
@@ -165,40 +165,40 @@ def cleanup_old(self):  # noqa
         """
         Cleanup any old resources from Kubernetes
 
-        This includes any RCs that are no longer considered the latest release (just a safety net)
+        This includes any RSs that are no longer considered the latest release (just a safety net)
         Secrets no longer tied to any ReplicaSet
         Stray pods no longer relevant to the latest release
         """
         latest_version = 'v{}'.format(self.version)
         self.app.log(
-            'Cleaning up RCs for releases older than {} (latest)'.format(latest_version),
+            'Cleaning up RSs for releases older than {} (latest)'.format(latest_version),
             level=logging.DEBUG
         )
 
         # Cleanup controllers
         labels = {'heritage': 'drycc'}
-        controller_removal = []
-        controllers = self._scheduler.rc.get(self.app.id, labels=labels).json()['items']
-        if not controllers:
-            controllers = []
-        for controller in controllers:
-            current_version = controller['metadata']['labels']['version']
+        replica_sets_removal = []
+        replica_sets = self._scheduler.rs.get(self.app.id, labels=labels).json()['items']
+        if not replica_sets:
+            replica_sets = []
+        for replica_set in replica_sets:
+            current_version = replica_set['metadata']['labels']['version']
             # skip the latest release
             if current_version == latest_version:
                 continue
 
             # aggregate versions together to removal all at once
-            if current_version not in controller_removal:
-                controller_removal.append(current_version)
+            if current_version not in replica_sets_removal:
+                replica_sets_removal.append(current_version)
 
-        if controller_removal:
+        if replica_sets_removal:
             self.app.log(
-                'Found the following versions to cleanup: {}'.format(', '.join(controller_removal)),  # noqa
+                'Found the following versions to cleanup: {}'.format(', '.join(replica_sets_removal)),  # noqa
                 level=logging.DEBUG
             )
 
         # this is RC related
-        for version in controller_removal:
+        for version in replica_sets_removal:
             self._delete_release_in_scheduler(self.app.id, version)
 
         # handle Deployments specific cleanups
@@ -269,7 +269,7 @@ def _delete_release_in_scheduler(self, namespace, version):
         """
         Deletes a specific release in k8s based on ReplicationController
 
-        Scale RCs to 0 then delete RCs and the version specific
+        Scale RSs to 0 then delete RSs and the version specific
         secret that container the env var
         """
         labels = {
@@ -281,14 +281,14 @@ def _delete_release_in_scheduler(self, namespace, version):
         # see if the app config has deploy timeout preference, otherwise use global
         timeout = self.config.values.get('DRYCC_DEPLOY_TIMEOUT', settings.DRYCC_DEPLOY_TIMEOUT)
 
-        controllers = self._scheduler.rc.get(namespace, labels=labels).json()['items']
-        if not controllers:
-            controllers = []
-        for controller in controllers:
-            # Deployment takes care of this in the API, RC does not
-            # Have the RC scale down pods and delete itself
-            self._scheduler.rc.scale(namespace, controller['metadata']['name'], 0, timeout)
-            self._scheduler.rc.delete(namespace, controller['metadata']['name'])
+        replica_sets = self._scheduler.rs.get(namespace, labels=labels).json()['items']
+        if not replica_sets:
+            replica_sets = []
+        for replica_set in replica_sets:
+            # Deployment takes care of this in the API, RS does not
+            # Have the RS scale down pods and delete itself
+            self._scheduler.rs.scale(namespace, replica_set['metadata']['name'], 0, timeout)
+            self._scheduler.rs.delete(namespace, replica_set['metadata']['name'])
 
     def save(self, *args, **kwargs):  # noqa
         if not self.summary:
diff --git a/rootfs/api/monitor.py b/rootfs/api/monitor.py
index 69c904dbd..569798b2c 100644
--- a/rootfs/api/monitor.py
+++ b/rootfs/api/monitor.py
@@ -14,8 +14,8 @@
 ON kubernetes_pod_network.tag_id = kubernetes_pod_network_tag.tag_id
 WHERE
   namespace in ({namespace_range})
-  AND time < to_timestamp({start})
-  AND time > to_timestamp({stop})
+  AND time > to_timestamp({start})
+  AND time < to_timestamp({stop})
 GROUP by namespace, pod_name
 """
 
@@ -34,8 +34,8 @@
   WHERE
     namespace='{namespace}'
     AND container_name='{container_name}'
-    AND time < to_timestamp({start})
-    AND time > to_timestamp({stop})
+    AND time > to_timestamp({start})
+    AND time < to_timestamp({stop})
   GROUP BY namespace, pod_name, container_name, kubernetes_pod_container.tag_id
 ) AS container
 GROUP BY namespace, container_name
@@ -56,8 +56,8 @@
 WHERE
   namespace='{namespace}'
   AND container_name='{container_name}'
-  AND time < to_timestamp({start})
-  AND time > to_timestamp({stop})
+  AND time > to_timestamp({start})
+  AND time < to_timestamp({stop})
 GROUP BY namespace, pod_name, container_name, timestamp
 """
 
@@ -76,8 +76,8 @@
 WHERE
   namespace='{namespace}'
   AND container_name='{container_name}'
-  AND time < to_timestamp({start})
-  AND time > to_timestamp({stop})
+  AND time > to_timestamp({start})
+  AND time < to_timestamp({stop})
 GROUP BY namespace, pod_name, container_name, timestamp
 """
 
@@ -95,8 +95,8 @@
 WHERE
   namespace='{namespace}'
   AND pod_name like '{pod_name_prefix}%'
-  AND time < to_timestamp({start})
-  AND time > to_timestamp({stop})
+  AND time > to_timestamp({start})
+  AND time < to_timestamp({stop})
 GROUP by namespace, pod_name, timestamp
 """
 
diff --git a/rootfs/scheduler/resources/rs.py b/rootfs/scheduler/resources/rs.py
index afe966d30..bde61d188 100644
--- a/rootfs/scheduler/resources/rs.py
+++ b/rootfs/scheduler/resources/rs.py
@@ -1,3 +1,4 @@
+import time
 from scheduler.exceptions import KubeHTTPException
 from scheduler.resources import Resource
 
@@ -27,3 +28,59 @@ def get(self, namespace, name=None, **kwargs):
             raise KubeHTTPException(response, message, *args)
 
         return response
+
+    def delete(self, namespace, name):
+        url = self.api("/namespaces/{}/replicasets/{}", namespace, name)
+        response = self.http_delete(url)
+        if self.unhealthy(response.status_code):
+            raise KubeHTTPException(
+                response,
+                'delete ReplicationController "{}" in Namespace "{}"', name, namespace
+            )
+
+        return response
+
+    def scale(self, namespace, name, desired, timeout):
+        rs = self.get(namespace, name).json()
+
+        current = int(rs['spec']['replicas'])
+        if desired == current:
+            self.log(namespace, "Not scaling RC {} to {} replicas. Already at desired replicas".format(name, desired))  # noqa
+            return
+        elif desired != rs['spec']['replicas']:  # RC needs new replica count
+            self.log(namespace, "scaling RC {} from {} to {} replicas".format(name, current, desired))  # noqa
+            self.scales.update(namespace, name, desired, rs)
+            self.wait_until_updated(namespace, name)
+
+        # Double check enough pods are in the required state to service the application
+        labels = rs['metadata']['labels']
+        containers = rs['spec']['template']['spec']['containers']
+        self.pods.wait_until_ready(namespace, containers, labels, desired, timeout)
+
+        # if it was a scale down operation, wait until terminating pods are done
+        if int(desired) < int(current):
+            self.pods.wait_until_terminated(namespace, labels, current, desired)
+
+    def wait_until_updated(self, namespace, name):
+        """
+        Looks at status/observedGeneration and metadata/generation and
+        waits for observedGeneration >= generation to happen, indicates RC is ready
+
+        More information is also available at:
+        https://github.com/kubernetes/kubernetes/blob/master/docs/devel/api-conventions.md#metadata
+        """
+        self.log(namespace, "waiting for ReplicationController {} to get a newer generation (30s timeout)".format(name), 'DEBUG')  # noqa
+        for _ in range(30):
+            try:
+                rs = self.get(namespace, name).json()
+                if (
+                    "observedGeneration" in rs["status"] and
+                    rs["status"]["observedGeneration"] >= rs["metadata"]["generation"]
+                ):
+                    self.log(namespace, "ReplicationController {} got a newer generation (30s timeout)".format(name), 'DEBUG')  # noqa
+                    break
+
+                time.sleep(1)
+            except KubeHTTPException as e:
+                if e.response.status_code == 404:
+                    time.sleep(1)
