Repository: goblet/goblet (language: python)

Issue:
[Cloudrun] Deploy goblet to cloudrun
It would be nice to have goblet running from a container in cloudrun along with api gateway with routes pointing to the cloudrun setup. This would be useful for applications that need the scaling/ throughput of cloudrun. 
Would need to be production ready with [gunicorn](https://gunicorn.org/). 


Discussion / hints:
Great feature ! Since Cloud Functions don't support Docker Images, it could be a nice work-around. 
[Zappa](https://github.com/Miserlou/Zappa), which is a solution similar to [Chalice](https://github.com/aws/chalice) offers the ability to deploy in a Docker image as a runtime for AWS Lambda.

Fix patch:
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..5c3b49e4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+
+# https://hub.docker.com/_/python
+FROM python:3.7-slim
+
+# Copy local code to the container image.
+ENV APP_HOME /app
+WORKDIR $APP_HOME
+COPY . .
+
+# Install dependencies.
+RUN pip install -r requirements.txt
+
+# Run the web service on container startup.
+CMD exec functions-framework --target=goblet_entrypoint
diff --git a/README.md b/README.md
index 1cee58ba..e665201f 100644
--- a/README.md
+++ b/README.md
@@ -208,7 +208,6 @@ Please file any issues, bugs or feature requests as an issue on our [GitHub](htt
  &#9745; Configuration Options (function names, ...) \
  &#9745; Use checksum for updates \
  &#9744; Generate [Openapi](https://github.com/OpenAPITools/openapi-generator)  clients \
- &#9745; User generated dataclasses for openapi spec \
  &#9745; [Scheduler](https://cloud.google.com/scheduler) trigger \
  &#9745; [Pub Sub](https://cloud.google.com/pubsub/docs/overview) trigger \
  &#9745; [Cloud Storage](https://cloud.google.com/functions/docs/calling/storage) trigger \
diff --git a/docs/source/backends.rst b/docs/source/backends.rst
new file mode 100644
index 00000000..42a361af
--- /dev/null
+++ b/docs/source/backends.rst
@@ -0,0 +1,37 @@
+========
+Backends
+========
+
+Cloudfunction
+^^^^^^^^^^^^^
+
+The default backend. 
+
+Cloudrun
+^^^^^^^^
+
+You can deploy your function to cloudrun updating the backend paramter to main Goblet class.
+
+.. code::python
+
+    app = Goblet(backend="cloudrun")
+
+.. note:: 
+
+    The function name for cloudrun must use only lowercase alphanumeric characters and dashes, cannot begin or end with a dash, and cannot be longer than 63 characters
+
+You can pass in configurations to you cloudrun deployment in the `cloudrun` section in your `config.json`. Parameters are 
+key, value pairs that will be parsed and passed into the `gcloud run deploy command <https://cloud.google.com/sdk/gcloud/reference/run/deploy>`__. For flags pass in an empty string. 
+
+.. code::json 
+
+    {
+        "cloudrun":{
+            "max-instances": "1",
+            "no-allow-authenticated: ""
+        }
+    }
+
+Deploying to cloudrun requires either a Dockerfile or Procfile in the directory you are looking to deploy your goblet app. If neither
+of those files are found, then goblet will create a default Dockerfile that allows the app to be build, deployed, and run correctly. 
+Having a custom Dockerfile if only needed if you would like to customize you container at all. 
\ No newline at end of file
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 97b9718d..83ca8fca 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -95,6 +95,14 @@ Resources
 
    resources
 
+Backends
+----------
+
+.. toctree::
+   :maxdepth: 2
+
+   backends
+
 Blogs
 --------
 
diff --git a/goblet/__version__.py b/goblet/__version__.py
index 6b472ed3..42174f6f 100644
--- a/goblet/__version__.py
+++ b/goblet/__version__.py
@@ -1,4 +1,4 @@
-VERSION = (0, 5, 2)
+VERSION = (0, 6, 0)
 
 
 __version__ = '.'.join(map(str, VERSION))
diff --git a/goblet/app.py b/goblet/app.py
index 27f1b333..115d405f 100644
--- a/goblet/app.py
+++ b/goblet/app.py
@@ -13,9 +13,9 @@ class Goblet(Register_Handlers):
     Main class which inherits most of its logic from the Register_Handlers class. Local param is used
     to set the entrypoint for running goblet locally
     """
-    def __init__(self, function_name="goblet", local="local", cors=None):
+    def __init__(self, function_name="goblet", backend="cloudfunction", local="local", cors=None):
         self.function_name = GConfig().function_name or function_name
-        super(Goblet, self).__init__(function_name=self.function_name, cors=cors)
+        super(Goblet, self).__init__(function_name=self.function_name, backend=backend, cors=cors)
         self.log = logging.getLogger(__name__)
         self.headers = {}
         self.g = G()
diff --git a/goblet/cli.py b/goblet/cli.py
index 6591ddcc..985a7cd8 100644
--- a/goblet/cli.py
+++ b/goblet/cli.py
@@ -5,7 +5,8 @@
 import subprocess
 import json
 
-from goblet.utils import get_goblet_app, create_goblet_dir
+from goblet.utils import get_goblet_app
+from goblet.write_files import create_goblet_dir
 from goblet.deploy import Deployer
 from goblet.client import get_default_project
 from goblet.__version__ import __version__
diff --git a/goblet/common_cloud_actions.py b/goblet/common_cloud_actions.py
new file mode 100644
index 00000000..2ddf4ec1
--- /dev/null
+++ b/goblet/common_cloud_actions.py
@@ -0,0 +1,125 @@
+import google_auth_httplib2
+import logging
+import json
+from urllib.parse import quote_plus
+from googleapiclient.errors import HttpError
+
+from goblet.config import GConfig
+from goblet.client import Client, get_default_project, get_default_location, get_credentials
+
+log = logging.getLogger('goblet.deployer')
+log.setLevel(logging.INFO)
+
+
+def create_cloudfunction(req_body, config=None):
+    """Creates a cloudfunction based on req_body"""
+    function_name = req_body['name'].split('/')[-1]
+    function_client = Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}')
+    try:
+        resp = function_client.execute('create', parent_key="location", params={'body': req_body})
+        log.info(f"creating cloudfunction {function_name}")
+    except HttpError as e:
+        if e.resp.status == 409:
+            log.info(f"updating cloudfunction {function_name}")
+            resp = function_client.execute('patch', parent_key="name", parent_schema=req_body["name"], params={'body': req_body})
+        else:
+            raise e
+    function_client.wait_for_operation(resp["name"], calls="operations")
+
+    # Set IAM Bindings
+    config = GConfig(config=config)
+    if config.bindings:
+        policy_client = Client("cloudfunctions", 'v1', calls='projects.locations.functions',
+                               parent_schema=req_body['name'])
+
+        log.info(f"adding IAM bindings for cloudfunction {function_name}")
+        policy_bindings = {
+            'policy': {'bindings': config.bindings}
+        }
+        resp = policy_client.execute('setIamPolicy', parent_key="resource", params={'body': policy_bindings})
+
+
+def destroy_cloudfunction(name):
+    """Destroys cloudfunction"""
+    try:
+        client = Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}/functions/' + name)
+        client.execute('delete', parent_key="name")
+        log.info(f"deleting google cloudfunction {name}......")
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info(f"cloudfunction {name} already destroyed")
+        else:
+            raise e
+
+
+def destroy_cloudrun(name):
+    """Destroys cloudrun"""
+    try:
+        client = Client("run", 'v1', calls='projects.locations.services', parent_schema='projects/{project_id}/locations/{location_id}/services/' + name)
+        client.execute('delete', parent_key="name")
+        log.info(f"deleting cloudrun {name}......")
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info(f"cloudrun {name} already destroyed")
+        else:
+            raise e
+
+
+def destroy_cloudfunction_artifacts(name):
+    """Destroys all images stored in cloud storage that are related to the function."""
+    client = Client("cloudresourcemanager", 'v1', calls='projects')
+    resp = client.execute('get', parent_key='projectId', parent_schema=get_default_project())
+    project_number = resp["projectNumber"]
+    region = get_default_location()
+    if not region:
+        raise Exception("Missing Region")
+    bucket_name = f"gcf-sources-{project_number}-{get_default_location()}"
+    http = client.http or google_auth_httplib2.AuthorizedHttp(get_credentials())
+    resp = http.request(f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o?prefix={name}")
+    objects = json.loads(resp[1])
+    if not objects.get("items"):
+        log.info("Artifacts already deleted")
+        return
+    for storage in objects["items"]:
+        log.info(f"Deleting artifact {storage['name']}")
+        resp = http.request(f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o/{quote_plus(storage['name'])}", method="DELETE")
+
+
+def get_cloudrun_url(name):
+    """Get the cloudrun url"""
+    try:
+        client = Client("run", 'v1', calls='projects.locations.services', parent_schema='projects/{project_id}/locations/{location_id}/services/' + name)
+        resp = client.execute('get', parent_key="name")
+        return resp["status"]["url"]
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info(f"cloudrun {name} not found")
+        else:
+            raise e
+
+
+def create_pubsub_subscription(sub_name, req_body):
+    """Creates a pubsub subscription from req_body"""
+    pubsub_client = Client("pubsub", 'v1', calls='projects.subscriptions', parent_schema='projects/{project_id}/subscriptions/' + sub_name)
+    try:
+        pubsub_client.execute('create', parent_key="name", params={'body': req_body})
+        log.info(f"creating pubsub subscription {sub_name}")
+    except HttpError as e:
+        if e.resp.status == 409:
+            log.info(f"updating pubsub subscription {sub_name}")
+            pubsub_client.execute('patch', parent_key="name", parent_schema='projects/{project_id}/subscriptions/' + sub_name, params={'body': req_body})
+        else:
+            raise e
+
+
+def destroy_pubsub_subscription(name):
+    """Destroys pubsub subscription"""
+    try:
+        client = Client("pubsub", 'v1', calls='projects.subscriptions', parent_schema='projects/{project_id}/subscriptions/' + name)
+        client.execute('delete', parent_key="subscription")
+        log.info(f"deleting pubsub subscription {name}......")
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info(f"pubsub subscription {name} already destroyed")
+        else:
+            raise e
diff --git a/goblet/decorators.py b/goblet/decorators.py
index 99871bf6..be4d8d53 100644
--- a/goblet/decorators.py
+++ b/goblet/decorators.py
@@ -9,6 +9,7 @@
 log = logging.getLogger(__name__)
 
 EVENT_TYPES = ['all', 'http', 'schedule', 'pubsub', 'storage', 'route']
+BACKEND_TYPES = ["cloudfunction", "cloudrun"]
 
 
 class DecoratorAPI:
@@ -82,13 +83,17 @@ def register_middleware(self, func, event_type='all'):
 class Register_Handlers(DecoratorAPI):
     """Core Goblet logic. App entrypoint is the __call__ function which routes the request to the corresonding handler class"""
 
-    def __init__(self, function_name, cors=None):
+    def __init__(self, function_name, backend="cloudfunction", cors=None):
+        self.backend = backend
+        if backend not in BACKEND_TYPES:
+            raise ValueError(f"{backend} not a valid backend")
+
         self.handlers = {
-            "route": ApiGateway(function_name, cors=cors),
-            "schedule": Scheduler(function_name),
-            "pubsub": PubSub(function_name),
-            "storage": Storage(function_name),
-            "http": HTTP()
+            "route": ApiGateway(function_name, cors=cors, backend=backend),
+            "schedule": Scheduler(function_name, backend=backend),
+            "pubsub": PubSub(function_name, backend=backend),
+            "storage": Storage(function_name, backend=backend),
+            "http": HTTP(backend=backend)
         }
         self.middleware_handlers = {}
         self.current_request = None
@@ -151,23 +156,22 @@ def _register_handler(self, handler_type, name,
             kwargs=kwargs,
         )
 
-    def deploy(self, sourceUrl):
+    def deploy(self, source_url):
         """Call each handlers deploy method"""
         for k, v in self.handlers.items():
             log.info(f"deploying {k}")
-            # TODO: better handle entrypoint
-            v.deploy(sourceUrl, "goblet_entrypoint")
+            v.deploy(source_url, entrypoint="goblet_entrypoint")
 
     def destroy(self):
         """Call each handlers destroy method"""
         for k, v in self.handlers.items():
-            log.info(f"deploying {k}")
+            log.info(f"destroying {k}")
             v.destroy()
 
     def is_http(self):
         """Is http determines if additional cloudfunctions will be needed since triggers other than http will require their own
         function"""
-        if len(self.handlers["route"].routes) > 0 or len(self.handlers["schedule"].jobs) > 0 or self.handlers["http"].http:
+        if len(self.handlers["route"].resources) > 0 or len(self.handlers["schedule"].resources) > 0 or self.handlers["http"].resources:
             return True
         return False
 
diff --git a/goblet/deploy.py b/goblet/deploy.py
index 4175f5b2..03cdef7b 100644
--- a/goblet/deploy.py
+++ b/goblet/deploy.py
@@ -1,21 +1,22 @@
 from pathlib import Path
 import zipfile
 import os
+import sys
 import requests
 import logging
 import hashlib
 from requests import request
 import base64
-import json
-from urllib.parse import quote_plus
 import warnings
+import subprocess
 
 from googleapiclient.errors import HttpError
 
-from goblet.client import Client, get_default_project, get_default_location, get_credentials
+from goblet.client import Client, get_default_project, get_default_location
+from goblet.common_cloud_actions import create_cloudfunction, destroy_cloudfunction, destroy_cloudfunction_artifacts, destroy_cloudrun
 from goblet.utils import get_dir, get_g_dir, checksum
+from goblet.write_files import write_dockerfile
 from goblet.config import GConfig
-import google_auth_httplib2
 
 log = logging.getLogger('goblet.deployer')
 log.setLevel(logging.INFO)
@@ -28,12 +29,13 @@ def __init__(self, config={}):
         self.config = config
         if not config:
             self.config = {
-                "name": "goblet_test_app"
+                "name": "goblet"
             }
         self.name = self.config["name"]
         self.zipf = self.create_zip()
         self.function_client = self._create_function_client()
         self.func_name = f"projects/{get_default_project()}/locations/{get_default_location()}/functions/{self.name}"
+        self.run_name = f"projects/{get_default_project()}/locations/{get_default_location()}/services/{self.name}"
 
     def _create_function_client(self):
         return Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}')
@@ -43,28 +45,32 @@ def package(self):
 
     def deploy(self, goblet, skip_function=False, only_function=False, config=None, force=False):
         """Deploys http cloudfunction and then calls goblet.deploy() to deploy any handler's required infrastructure"""
-        url = None
+        source_url = None
         if not skip_function:
-            log.info("zipping function......")
-            self.zip()
-            if not force and self.get_function() and not self._cloudfunction_delta(f'.goblet/{self.name}.zip'):
-                log.info("No changes detected......")
-            else:
-                log.info("uploading function zip to gs......")
-                url = self._upload_zip()
-                if goblet.is_http():
-                    # TODO: temporary workaround around the goblet entrypoint.
-                    self.create_function(url, "goblet_entrypoint", config)
-                    # self.create_function(url, goblet.entrypoint, config)
-        if not only_function and url:
-            goblet.deploy(url)
+            if goblet.backend == "cloudfunction":
+                log.info("zipping function......")
+                self.zip()
+                if not force and self.get_function() and not self._cloudfunction_delta(f'.goblet/{self.name}.zip'):
+                    log.info("No changes detected......")
+                else:
+                    log.info("uploading function zip to gs......")
+                    source_url = self._upload_zip()
+                    if goblet.is_http():
+                        self.create_function(source_url, "goblet_entrypoint", config)
+            if goblet.backend == "cloudrun":
+                self.create_cloudrun(config)
+        if not only_function:
+            goblet.deploy(source_url)
 
         return goblet
 
     def destroy(self, goblet, all=None):
         """Destroys http cloudfunction and then calls goblet.destroy() to remove handler's infrastructure"""
         goblet.destroy()
-        destroy_cloudfunction(self.name)
+        if goblet.backend == "cloudfunction":
+            destroy_cloudfunction(self.name)
+        if goblet.backend == "cloudrun":
+            destroy_cloudrun(self.name)
         if all:
             destroy_cloudfunction_artifacts(self.name)
 
@@ -93,6 +99,41 @@ def create_function(self, url, entrypoint, config=None):
         }
         create_cloudfunction(req_body, config=config.config)
 
+    def create_cloudrun(self, config=None):
+        """Creates http cloudfunction"""
+        config = GConfig(config=config)
+        cloudrun_configs = config.cloudrun or {}
+        if not cloudrun_configs.get("no-allow-unauthenticated") or cloudrun_configs.get("allow-unauthenticated"):
+            cloudrun_configs["no-allow-unauthenticated"] = None
+        cloudrun_options = []
+        for k, v in cloudrun_configs.items():
+            cloudrun_options.append(f"--{k}")
+            if v:
+                cloudrun_options.append(v)
+
+        base_command = ["gcloud", "run", "deploy", self.name, "--project", get_default_project(), "--region", get_default_location(), "--source",
+                        get_dir(), "--command", "functions-framework,--target=goblet_entrypoint", "--port", "8080"]
+        base_command.extend(cloudrun_options)
+        try:
+            if not os.path.exists(get_dir() + "/Dockerfile") and not os.path.exists(get_dir() + "/Procfile"):
+                log.info("No Dockerfile or Procfile found for cloudrun backend. Writing default Dockerfile")
+                write_dockerfile()
+            subprocess.check_output(base_command, env=os.environ)
+        except subprocess.CalledProcessError:
+            log.error("Error during cloudrun deployment while running the following command")
+            log.error((" ").join(base_command))
+            sys.exit(1)
+
+        # Set IAM Bindings
+        if config.bindings:
+            policy_client = Client("run", 'v1', calls='projects.locations.services', parent_schema=self.run_name)
+
+            log.info(f"adding IAM bindings for cloudrun {self.name}")
+            policy_bindings = {
+                'policy': {'bindings': config.bindings}
+            }
+            policy_client.execute('setIamPolicy', parent_key="resource", params={'body': policy_bindings})
+
     def _cloudfunction_delta(self, filename):
         """Compares md5 hash between local zipfile and cloudfunction already deployed"""
         self.zipf.close()
@@ -155,64 +196,3 @@ def zip_directory(self, dir, include=['*.py'], exclude=['build', 'docs', 'exampl
         for path in globbed_files:
             if not set(path.parts).intersection(exclusion_set):
                 self.zipf.write(str(path))
-
-
-def create_cloudfunction(req_body, config=None):
-    """Creates a cloudfunction based on req_body"""
-    function_name = req_body['name'].split('/')[-1]
-    function_client = Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}')
-    try:
-        resp = function_client.execute('create', parent_key="location", params={'body': req_body})
-        log.info(f"creating cloudfunction {function_name}")
-    except HttpError as e:
-        if e.resp.status == 409:
-            log.info(f"updating cloudfunction {function_name}")
-            resp = function_client.execute('patch', parent_key="name", parent_schema=req_body["name"], params={'body': req_body})
-        else:
-            raise e
-    function_client.wait_for_operation(resp["name"], calls="operations")
-
-    # Set IAM Bindings
-    config = GConfig(config=config)
-    if config.bindings:
-        policy_client = Client("cloudfunctions", 'v1', calls='projects.locations.functions',
-                               parent_schema=req_body['name'])
-
-        log.info(f"adding IAM bindings for cloudfunction {function_name}")
-        policy_bindings = {
-            'policy': {'bindings': config.bindings}
-        }
-        resp = policy_client.execute('setIamPolicy', parent_key="resource", params={'body': policy_bindings})
-
-
-def destroy_cloudfunction(name):
-    """Destroys cloudfunction"""
-    try:
-        client = Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}/functions/' + name)
-        client.execute('delete', parent_key="name")
-        log.info(f"deleting google cloudfunction {name}......")
-    except HttpError as e:
-        if e.resp.status == 404:
-            log.info(f"cloudfunction {name} already destroyed")
-        else:
-            raise e
-
-
-def destroy_cloudfunction_artifacts(name):
-    """Destroys all images stored in cloud storage that are related to the function."""
-    client = Client("cloudresourcemanager", 'v1', calls='projects')
-    resp = client.execute('get', parent_key='projectId', parent_schema=get_default_project())
-    project_number = resp["projectNumber"]
-    region = get_default_location()
-    if not region:
-        raise Exception("Missing Region")
-    bucket_name = f"gcf-sources-{project_number}-{get_default_location()}"
-    http = client.http or google_auth_httplib2.AuthorizedHttp(get_credentials())
-    resp = http.request(f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o?prefix={name}")
-    objects = json.loads(resp[1])
-    if not objects.get("items"):
-        log.info("Artifacts already deleted")
-        return
-    for storage in objects["items"]:
-        log.info(f"Deleting artifact {storage['name']}")
-        resp = http.request(f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o/{quote_plus(storage['name'])}", method="DELETE")
diff --git a/goblet/handler.py b/goblet/handler.py
index c8aa4b6c..c0fc17e3 100644
--- a/goblet/handler.py
+++ b/goblet/handler.py
@@ -1,7 +1,24 @@
+import logging
+
+log = logging.getLogger('goblet.deployer')
+log.setLevel(logging.INFO)
+
+
 class Handler:
     """Base Handler class"""
 
+    valid_backends = []
+    resources = None
+    resource_type = ""
+    backend = "cloudfunction"
+
     def deploy(self, sourceUrl=None, entrypoint=None):
+        if self.resources and self.backend not in self.valid_backends:
+            log.info(f"skipping... {self.backend} not supported for {self.resource_type}")
+            return
+        self._deploy(sourceUrl, entrypoint)
+
+    def _deploy(self, sourceUrl=None, entrypoint=None):
         raise NotImplementedError("deploy")
 
     def destroy(self):
@@ -10,5 +27,7 @@ def destroy(self):
     def __call__(self, request, context=None):
         raise NotImplementedError("__call__")
 
-    def __add__(self):
-        raise NotImplementedError("__add__")
+    def __add__(self, other):
+        if other.resources:
+            self.resources.update(other.resources)
+        return self
diff --git a/goblet/resources/http.py b/goblet/resources/http.py
index 24d6129f..b4ae815f 100644
--- a/goblet/resources/http.py
+++ b/goblet/resources/http.py
@@ -3,27 +3,27 @@
 
 class HTTP(Handler):
     """Http Trigger"""
-    def __init__(self, http=None):
-        self.http = http or []
+
+    resource_type = "http"
+    valid_backends = ["cloudfunction", "cloudrun"]
+
+    def __init__(self, resources=None, backend="cloudfunction"):
+        self.resources = resources or []
+        self.backend = backend
 
     def register_http(self, func, kwargs):
-        self.http.append({"func": func, "headers": kwargs.get("headers", {})})
+        self.resources.append({"func": func, "headers": kwargs.get("headers", {})})
 
     def __call__(self, request, context=None):
         headers = request.headers or {}
-        for http_endpoint in self.http:
+        for http_endpoint in self.resources:
             endpoint_headers = http_endpoint["headers"]
             if isinstance(endpoint_headers, dict) and endpoint_headers.items() <= dict(headers.items()).items():
                 return http_endpoint["func"](request)
             if isinstance(endpoint_headers, set) and endpoint_headers <= set(headers.keys()):
                 return http_endpoint["func"](request)
 
-    def __add__(self, other):
-        if other.http:
-            self.http.extend(other.http)
-        return self
-
-    def deploy(self, sourceUrl=None, entrypoint=None):
+    def _deploy(self, sourceUrl=None, entrypoint=None):
         return
 
     def destroy(self):
diff --git a/goblet/resources/pubsub.py b/goblet/resources/pubsub.py
index ca7db089..098ed8f6 100644
--- a/goblet/resources/pubsub.py
+++ b/goblet/resources/pubsub.py
@@ -1,4 +1,5 @@
 import base64
+from goblet.common_cloud_actions import create_pubsub_subscription, destroy_pubsub_subscription, get_cloudrun_url
 from goblet.deploy import create_cloudfunction, destroy_cloudfunction
 
 from goblet.config import GConfig
@@ -16,22 +17,27 @@ class PubSub(Handler):
     """Pubsub topic trigger
     https://cloud.google.com/functions/docs/calling/pubsub
     """
-    def __init__(self, name, topics=None):
+
+    valid_backends = ["cloudfunction", "cloudrun"]
+    resource_type = "pubsub"
+
+    def __init__(self, name, resources=None, backend="cloudfunction"):
         self.name = name
+        self.backend = backend
         self.cloudfunction = f"projects/{get_default_project()}/locations/{get_default_location()}/functions/{name}"
-        self.topics = topics or {}
+        self.resources = resources or {}
 
     def register_topic(self, name, func, kwargs):
         topic = kwargs["topic"]
         kwargs = kwargs.pop('kwargs')
         attributes = kwargs.get("attributes", {})
-        if self.topics.get(topic):
-            self.topics[topic][name] = {
+        if self.resources.get(topic):
+            self.resources[topic][name] = {
                 "func": func,
                 "attributes": attributes
             }
         else:
-            self.topics[topic] = {
+            self.resources[topic] = {
                 name: {
                     "func": func,
                     "attributes": attributes
@@ -43,7 +49,7 @@ def __call__(self, event, context):
         data = base64.b64decode(event['data']).decode('utf-8')
         attributes = event.get("attributes") or {}
 
-        topic = self.topics.get(topic_name)
+        topic = self.resources.get(topic_name)
         if not topic:
             raise ValueError(f"Topic {topic_name} not found")
 
@@ -53,18 +59,46 @@ def __call__(self, event, context):
                 info["func"](data)
         return
 
-    def __add__(self, other):
-        self.topics.update(other.topics)
-        return self
-
-    def deploy(self, sourceUrl=None, entrypoint=None):
-        if not self.topics:
+    def _deploy(self, sourceUrl=None, entrypoint=None, config={}):
+        if not self.resources:
             return
+        if self.backend == "cloudfunction":
+            self._deploy_cloudfunction(sourceUrl=sourceUrl, entrypoint=entrypoint)
+        if self.backend == "cloudrun":
+            self._deploy_cloudrun(config=config)
+
+    def _deploy_cloudrun(self, config={}):
+        log.info("deploying pubsub subscriptions......")
+        push_url = get_cloudrun_url(self.name)
+
+        config = GConfig(config=config)
+        if config.cloudrun and config.cloudrun.get("service-account"):
+            service_account = config.cloudrun.get("service-account")
+        elif config.pubsub and config.pubsub.get('serviceAccountEmail'):
+            service_account = config.pubsub.get('serviceAccountEmail')
+        else:
+            raise ValueError("Service account not found in cloudrun. You can set `serviceAccountEmail` field in config.json under `pubsub`")
+
+        for topic in self.resources:
+            sub_name = f"{self.name}-{topic}"
+            req_body = {
+                "name": sub_name,
+                "topic": f"projects/{get_default_project()}/topics/{topic}",
+                "pushConfig": {
+                    "pushEndpoint": push_url,
+                    "oidcToken": {
+                        "serviceAccountEmail": service_account,
+                        "audience": push_url
+                    }
+                }
+            }
+            create_pubsub_subscription(sub_name=sub_name, req_body=req_body)
 
+    def _deploy_cloudfunction(self, sourceUrl=None, entrypoint=None):
         log.info("deploying topic functions......")
         config = GConfig()
         user_configs = config.cloudfunction or {}
-        for topic in self.topics:
+        for topic in self.resources:
             req_body = {
                 "name": f"{self.cloudfunction}-topic-{topic}",
                 "description": config.description or "created by goblet",
@@ -80,5 +114,9 @@ def deploy(self, sourceUrl=None, entrypoint=None):
             create_cloudfunction(req_body)
 
     def destroy(self):
-        for topic in self.topics:
-            destroy_cloudfunction(f"{self.name}-topic-{topic}")
+        if self.backend == "cloudfunction":
+            for topic in self.resources:
+                destroy_cloudfunction(f"{self.name}-topic-{topic}")
+        if self.backend == "cloudrun":
+            for topic in self.resources:
+                destroy_pubsub_subscription(f"{self.name}-{topic}")
diff --git a/goblet/resources/routes.py b/goblet/resources/routes.py
index 4ca7af75..d603e494 100644
--- a/goblet/resources/routes.py
+++ b/goblet/resources/routes.py
@@ -14,6 +14,8 @@
 from goblet.client import Client, get_default_project, get_default_location
 from goblet.utils import get_g_dir
 from goblet.config import GConfig
+from goblet.common_cloud_actions import get_cloudrun_url
+
 from googleapiclient.errors import HttpError
 
 log = logging.getLogger('goblet.deployer')
@@ -24,9 +26,14 @@ class ApiGateway(Handler):
     """Api Gateway instance, which includes api, api config, api gateway instances
     https://cloud.google.com/api-gateway
     """
-    def __init__(self, app_name, routes=None, cors=None):
+
+    resource_type = "apigateway"
+    valid_backends = ["cloudfunction", "cloudrun"]
+
+    def __init__(self, app_name, resources=None, cors=None, backend="cloudfunction"):
+        self.backend = backend
         self.name = self.format_name(app_name)
-        self.routes = routes or {}
+        self.resources = resources or {}
         self.cors = cors or {}
         self._api_client = None
         self.cloudfunction = f"https://{get_default_location()}-{get_default_project()}.cloudfunctions.net/{self.name}"
@@ -47,7 +54,7 @@ def register_route(self, name, func, kwargs):
         kwargs = kwargs.pop('kwargs')
         if not kwargs.get("cors"):
             kwargs["cors"] = self.cors
-        path_entries = self.routes.get(path, {})
+        path_entries = self.resources.get(path, {})
         for method in methods:
             if path_entries.get(method):
                 raise ValueError(
@@ -55,31 +62,27 @@ def register_route(self, name, func, kwargs):
                     "between view functions: \"%s\" and \"%s\". A specific "
                     "method may only be specified once for "
                     "a particular path." % (
-                        method, path, self.routes[path][method].function_name,
+                        method, path, self.resources[path][method].function_name,
                         name)
                 )
             entry = RouteEntry(func, name, path, method, **kwargs)
             path_entries[method] = entry
-        self.routes[path] = path_entries
+        self.resources[path] = path_entries
 
     def __call__(self, request, context=None):
         method = request.method
         path = request.path
-        entry = self.routes.get(path, {}).get(method)
+        entry = self.resources.get(path, {}).get(method)
         if not entry:
             # test param paths
-            for p in self.routes:
+            for p in self.resources:
                 if '{' in p and self._matched_path(p, path):
-                    entry = self.routes.get(p, {}).get(method)
+                    entry = self.resources.get(p, {}).get(method)
         # TODO: better handling
         if not entry:
             raise ValueError(f"No route found for {path} with {method}")
         return entry(request)
 
-    def __add__(self, other):
-        self.routes.update(other.routes)
-        return self
-
     @staticmethod
     def _matched_path(org_path, path):
         split_org = re.sub(r'{[\w]+}', '', org_path).split('/')
@@ -106,11 +109,14 @@ def _create_gateway_client(self):
     def _patch_gateway_client(self):
         return Client("apigateway", 'v1', calls='projects.locations.gateways', parent_schema='projects/{project_id}/locations/{location_id}/gateways/' + self.name)
 
-    def deploy(self, sourceUrl=None, entrypoint=None):
-        if len(self.routes) == 0:
+    def _deploy(self, sourceUrl=None, entrypoint=None):
+        if len(self.resources) == 0:
             return
         log.info("deploying api......")
-        self.generate_openapi_spec(self.cloudfunction)
+        base_url = self.cloudfunction
+        if self.backend == "cloudrun":
+            base_url = get_cloudrun_url(self.name)
+        self.generate_openapi_spec(base_url)
         try:
             resp = self.api_client.execute('create', params={'apiId': self.name})
             self.api_client.wait_for_operation(resp["name"])
@@ -163,7 +169,7 @@ def deploy(self, sourceUrl=None, entrypoint=None):
         return
 
     def destroy(self):
-        if len(self.routes) == 0:
+        if len(self.resources) == 0:
             return
 
         # destroy api gateway
@@ -208,7 +214,7 @@ def destroy(self):
     def generate_openapi_spec(self, cloudfunction):
         config = GConfig()
         spec = OpenApiSpec(self.name, cloudfunction, security_definitions=config.securityDefinitions, security=config.security)
-        spec.add_apigateway_routes(self.routes)
+        spec.add_apigateway_routes(self.resources)
         with open(f'{get_g_dir()}/{self.name}_openapi_spec.yml', 'w') as f:
             spec.write(f)
 
diff --git a/goblet/resources/scheduler.py b/goblet/resources/scheduler.py
index f6db61da..d5a5be8d 100644
--- a/goblet/resources/scheduler.py
+++ b/goblet/resources/scheduler.py
@@ -2,6 +2,9 @@
 
 from goblet.handler import Handler
 from goblet.client import Client, get_default_project, get_default_location
+from goblet.common_cloud_actions import get_cloudrun_url
+from goblet.config import GConfig
+
 from googleapiclient.errors import HttpError
 
 log = logging.getLogger('goblet.deployer')
@@ -12,10 +15,15 @@ class Scheduler(Handler):
     """Cloud Scheduler job which calls http endpoint
     https://cloud.google.com/scheduler/docs
     """
-    def __init__(self, name, jobs=None):
+
+    resource_type = "scheduler"
+    valid_backends = ["cloudfunction", "cloudrun"]
+
+    def __init__(self, name, resources=None, backend="cloudfunction"):
         self.name = name
+        self.backend = backend
         self.cloudfunction = f"projects/{get_default_project()}/locations/{get_default_location()}/functions/{name}"
-        self.jobs = jobs or {}
+        self.resources = resources or {}
         self._api_client = None
 
     @property
@@ -39,12 +47,12 @@ def register_job(self, name, func, kwargs):
         attempt_deadline = kwargs.get("attemptDeadline")
 
         job_num = 1
-        if self.jobs.get(name):
+        if self.resources.get(name):
             # increment job_num if there is already a scheduled job for this func
-            job_num = self.jobs[name]["job_num"] + 1
-            self.jobs[name]["job_num"] = job_num
+            job_num = self.resources[name]["job_num"] + 1
+            self.resources[name]["job_num"] = job_num
             name = f"{name}-{job_num}"
-        self.jobs[name] = {
+        self.resources[name] = {
             "job_num": job_num,
             "job_json": {
                 "name": f"projects/{get_default_project()}/locations/{get_default_location()}/jobs/{self.name}-{name}",
@@ -76,29 +84,35 @@ def __call__(self, request, context=None):
         if not func_name:
             raise ValueError("No X-Goblet-Name header found")
 
-        job = self.jobs[func_name]
+        job = self.resources[func_name]
         if not job:
             raise ValueError(f"Function {func_name} not found")
         return job["func"]()
 
-    def __add__(self, other):
-        self.jobs.update(other.jobs)
-        return self
-
-    def deploy(self, sourceUrl=None, entrypoint=None):
-        if not self.jobs:
+    def _deploy(self, sourceUrl=None, entrypoint=None, config={}):
+        if not self.resources:
             return
 
-        cloudfunction_client = Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}')
-        resp = cloudfunction_client.execute('get', parent_key="name", parent_schema=self.cloudfunction)
-        if not resp:
-            raise ValueError(f"Function {self.cloudfunction} not found")
-        cloudfunction_target = resp["httpsTrigger"]["url"]
-        service_account = resp["serviceAccountEmail"]
-
+        if self.backend == "cloudfunction":
+            cloudfunction_client = Client("cloudfunctions", 'v1', calls='projects.locations.functions', parent_schema='projects/{project_id}/locations/{location_id}')
+            resp = cloudfunction_client.execute('get', parent_key="name", parent_schema=self.cloudfunction)
+            if not resp:
+                raise ValueError(f"Function {self.cloudfunction} not found")
+            target = resp["httpsTrigger"]["url"]
+            service_account = resp["serviceAccountEmail"]
+
+        if self.backend == "cloudrun":
+            target = get_cloudrun_url(self.name)
+            config = GConfig(config=config)
+            if config.cloudrun and config.cloudrun.get("service-account"):
+                service_account = config.cloudrun.get("service-account")
+            elif config.scheduler and config.scheduler.get('serviceAccount'):
+                service_account = config.scheduler.get('serviceAccount')
+            else:
+                raise ValueError("Service account not found in cloudrun. You can set `serviceAccount` field in config.json under `scheduler`")
         log.info("deploying scheduled jobs......")
-        for job_name, job in self.jobs.items():
-            job["job_json"]["httpTarget"]['uri'] = cloudfunction_target
+        for job_name, job in self.resources.items():
+            job["job_json"]["httpTarget"]['uri'] = target
             job["job_json"]["httpTarget"]['oidcToken']["serviceAccountEmail"] = service_account
 
             self.deploy_job(job_name, job["job_json"])
@@ -115,9 +129,9 @@ def deploy_job(self, job_name, job):
                 raise e
 
     def destroy(self):
-        if not self.jobs:
+        if not self.resources:
             return
-        for job_name in self.jobs.keys():
+        for job_name in self.resources.keys():
             self._destroy_job(job_name)
 
     def _destroy_job(self, job_name):
diff --git a/goblet/resources/storage.py b/goblet/resources/storage.py
index d6b6efb9..d7f5430f 100644
--- a/goblet/resources/storage.py
+++ b/goblet/resources/storage.py
@@ -21,10 +21,15 @@ class Storage(Handler):
     """Storage trigger
     https://cloud.google.com/functions/docs/calling/storage
     """
-    def __init__(self, name, buckets=None):
+
+    resource_type = "storage"
+    valid_backends = ["cloudfunction"]
+
+    def __init__(self, name, resources=None, backend="cloudfunction"):
         self.name = name
+        self.backend = backend
         self.cloudfunction = f"projects/{get_default_project()}/locations/{get_default_location()}/functions/{name}"
-        self.buckets = buckets or []
+        self.resources = resources or []
 
     def validate_event_type(self, event_type):
         if event_type not in STORAGE_EVENT_TYPES:
@@ -34,7 +39,7 @@ def register_bucket(self, name, func, kwargs):
         bucket_name = kwargs["bucket"]
         event_type = kwargs["event_type"]
         self.validate_event_type(event_type)
-        self.buckets.append({
+        self.resources.append({
             "bucket": bucket_name,
             "event_type": event_type,
             "name": name,
@@ -45,7 +50,7 @@ def __call__(self, event, context):
         event_type = context.event_type.split('.')[-1]
         bucket_name = event['bucket']
 
-        matched_buckets = [b for b in self.buckets if b["bucket"] == bucket_name and b["event_type"] == event_type]
+        matched_buckets = [b for b in self.resources if b["bucket"] == bucket_name and b["event_type"] == event_type]
         if not matched_buckets:
             raise ValueError("No functions found")
         for b in matched_buckets:
@@ -54,17 +59,17 @@ def __call__(self, event, context):
         return
 
     def __add__(self, other):
-        self.buckets.extend(other.buckets)
+        self.resources.extend(other.resources)
         return self
 
-    def deploy(self, sourceUrl=None, entrypoint=None):
-        if not self.buckets:
+    def _deploy(self, sourceUrl=None, entrypoint=None):
+        if not self.resources or not sourceUrl:
             return
 
         log.info("deploying storage functions......")
         config = GConfig()
         user_configs = config.cloudfunction or {}
-        for bucket in self.buckets:
+        for bucket in self.resources:
             req_body = {
                 "name": f"{self.cloudfunction}-storage-{bucket['name']}-{bucket['event_type']}".replace('.', '-'),
                 "description": config.description or "created by goblet",
@@ -80,5 +85,5 @@ def deploy(self, sourceUrl=None, entrypoint=None):
             create_cloudfunction(req_body)
 
     def destroy(self):
-        for bucket in self.buckets:
+        for bucket in self.resources:
             destroy_cloudfunction(f"{self.name}-storage-{bucket['name']}-{bucket['event_type']}".replace('.', '-'))
diff --git a/goblet/utils.py b/goblet/utils.py
index befcb1ab..8c59e676 100644
--- a/goblet/utils.py
+++ b/goblet/utils.py
@@ -2,8 +2,6 @@
 import importlib.util
 import collections.abc
 from contextlib import contextmanager
-import json
-from goblet.__version__ import __version__
 
 
 @contextmanager
@@ -55,55 +53,6 @@ def get_dir():
     return os.path.realpath('.')
 
 
-def create_goblet_dir(name):
-    """Creates a new goblet directory with a sample main.py, requirements.txt, and config.json"""
-    try:
-        os.mkdir(get_g_dir())
-    except FileExistsError:
-        pass
-    with open(f'{get_g_dir()}/config.json', 'w') as f:
-        f.write(json.dumps({'cloudfunction': {}}, indent=4))
-    with open('requirements.txt', 'w') as f:
-        f.write(f'goblet-gcp=={__version__}')
-    with open('main.py', 'w') as f:
-        f.write(f"""
-from goblet import Goblet, jsonify
-
-app = Goblet(function_name="goblet-{name}")
-
-@app.http()
-def main(request):
-    return jsonify(request.json)
-
-# route
-# @app.route('/hello')
-# def home():
-#     return jsonify("goodbye")
-
-# schedule
-# @app.schedule('5 * * * *')
-# def scheduled_job():
-#     return jsonify("success")
-
-# pubsub topic
-# @app.topic('test_topic')
-# def topic(data):
-#     app.log.info(data)
-#     return
-""")
-    with open('README.md', 'w') as f:
-        f.write(f"""
-# goblet-{name}
-
-autocreated by goblet
-
-To test endpoints locally run `goblet local`
-To deploy cloudfunctions and other gcp resources defined in `main.py` run `goblet deploy`
-
-To check out goblet documentation go to [docs](https://anovis.github.io/goblet/docs/build/html/index.html)
-""")
-
-
 def nested_update(d, u):
     """
     Updates nested dictionary d with nested dictionary u
diff --git a/goblet/write_files.py b/goblet/write_files.py
new file mode 100644
index 00000000..149380d6
--- /dev/null
+++ b/goblet/write_files.py
@@ -0,0 +1,73 @@
+import json
+import os
+
+from goblet.utils import get_g_dir, get_dir
+from goblet.__version__ import __version__
+
+
+def create_goblet_dir(name):
+    """Creates a new goblet directory with a sample main.py, requirements.txt, and config.json"""
+    try:
+        os.mkdir(get_g_dir())
+    except FileExistsError:
+        pass
+    with open(f'{get_g_dir()}/config.json', 'w') as f:
+        f.write(json.dumps({'cloudfunction': {}}, indent=4))
+    with open('requirements.txt', 'w') as f:
+        f.write(f'goblet-gcp=={__version__}')
+    with open('main.py', 'w') as f:
+        f.write(f"""
+from goblet import Goblet, jsonify
+
+app = Goblet(function_name="goblet-{name}")
+
+@app.http()
+def main(request):
+    return jsonify(request.json)
+
+# route
+# @app.route('/hello')
+# def home():
+#     return jsonify("goodbye")
+
+# schedule
+# @app.schedule('5 * * * *')
+# def scheduled_job():
+#     return jsonify("success")
+
+# pubsub topic
+# @app.topic('test_topic')
+# def topic(data):
+#     app.log.info(data)
+#     return
+""")
+    with open('README.md', 'w') as f:
+        f.write(f"""
+# goblet-{name}
+
+autocreated by goblet
+
+To test endpoints locally run `goblet local`
+To deploy cloudfunctions and other gcp resources defined in `main.py` run `goblet deploy`
+
+To check out goblet documentation go to [docs](https://anovis.github.io/goblet/docs/build/html/index.html)
+""")
+
+
+def write_dockerfile():
+    with open(f'{get_dir()}/Dockerfile', 'w') as f:
+        f.write("""
+# https://hub.docker.com/_/python
+FROM python:3.7-slim
+
+# Copy local code to the container image.
+ENV APP_HOME /app
+WORKDIR $APP_HOME
+COPY . .
+
+# Install dependencies.
+RUN pip install -r requirements.txt
+
+# Run the web service on container startup.
+CMD exec functions-framework --target=goblet_entrypoint
+""")


Test patch:
diff --git a/goblet/tests/data/http/deployer-cloudrun-destroy/delete-v1-projects-goblet-locations-us-central1-services-goblet_1.json b/goblet/tests/data/http/deployer-cloudrun-destroy/delete-v1-projects-goblet-locations-us-central1-services-goblet_1.json
new file mode 100644
index 00000000..4deb112a
--- /dev/null
+++ b/goblet/tests/data/http/deployer-cloudrun-destroy/delete-v1-projects-goblet-locations-us-central1-services-goblet_1.json
@@ -0,0 +1,12 @@
+{
+  "headers": {},
+  "body": {
+    "status": "Success",
+    "details": {
+      "name": "goblet",
+      "group": "v1",
+      "kind": "service",
+      "uid": "e0069f86-97aa-4d7d-83a1-30031356c24e"
+    }
+  }
+}
\ No newline at end of file
diff --git a/goblet/tests/data/http/discovery/get-discovery-v1-apis-pubsub-v1-rest_1.json.bz2 b/goblet/tests/data/http/discovery/get-discovery-v1-apis-pubsub-v1-rest_1.json.bz2
new file mode 100644
index 00000000..0758b3eb
Binary files /dev/null and b/goblet/tests/data/http/discovery/get-discovery-v1-apis-pubsub-v1-rest_1.json.bz2 differ
diff --git a/goblet/tests/data/http/discovery/get-discovery-v1-apis-run-v1-rest_1.json.bz2 b/goblet/tests/data/http/discovery/get-discovery-v1-apis-run-v1-rest_1.json.bz2
new file mode 100644
index 00000000..f4d597ab
Binary files /dev/null and b/goblet/tests/data/http/discovery/get-discovery-v1-apis-run-v1-rest_1.json.bz2 differ
diff --git a/goblet/tests/data/http/pubsub-deploy-cloudrun/get-v1-projects-goblet-locations-us-central1-services-goblet_1.json b/goblet/tests/data/http/pubsub-deploy-cloudrun/get-v1-projects-goblet-locations-us-central1-services-goblet_1.json
new file mode 100644
index 00000000..0000b501
--- /dev/null
+++ b/goblet/tests/data/http/pubsub-deploy-cloudrun/get-v1-projects-goblet-locations-us-central1-services-goblet_1.json
@@ -0,0 +1,10 @@
+{
+  "headers": {},
+  "body": {
+    "apiVersion": "serving.knative.dev/v1",
+    "kind": "Service",
+    "status": {
+      "url": "https://goblet-12345.a.run.app"
+    }
+  }
+}
\ No newline at end of file
diff --git a/goblet/tests/data/http/pubsub-deploy-cloudrun/put-v1-projects-goblet-subscriptions-goblet-test_1.json b/goblet/tests/data/http/pubsub-deploy-cloudrun/put-v1-projects-goblet-subscriptions-goblet-test_1.json
new file mode 100644
index 00000000..3e633a7e
--- /dev/null
+++ b/goblet/tests/data/http/pubsub-deploy-cloudrun/put-v1-projects-goblet-subscriptions-goblet-test_1.json
@@ -0,0 +1,22 @@
+{
+  "headers": {},
+  "body": {
+    "name": "projects/goblet/subscriptions/goblet-test",
+    "topic": "projects/goblet/topics/test",
+    "pushConfig": {
+      "pushEndpoint": "https://goblet-12345.a.run.app",
+      "attributes": {
+        "x-goog-version": "v1"
+      },
+      "oidcToken": {
+        "serviceAccountEmail": "SERVICE_ACCOUNT@developer.gserviceaccount.com",
+        "audience": "https://goblet-12345.a.run.app"
+      }
+    },
+    "ackDeadlineSeconds": 10,
+    "messageRetentionDuration": "604800s",
+    "expirationPolicy": {
+      "ttl": "2678400s"
+    }
+  }
+}
\ No newline at end of file
diff --git a/goblet/tests/data/http/pubsub-destroy-cloudrun/delete-v1-projects-goblet-subscriptions-goblet-test_1.json b/goblet/tests/data/http/pubsub-destroy-cloudrun/delete-v1-projects-goblet-subscriptions-goblet-test_1.json
new file mode 100644
index 00000000..57238766
--- /dev/null
+++ b/goblet/tests/data/http/pubsub-destroy-cloudrun/delete-v1-projects-goblet-subscriptions-goblet-test_1.json
@@ -0,0 +1,4 @@
+{
+  "headers": {},
+  "body": {}
+}
\ No newline at end of file
diff --git a/goblet/tests/data/http/schedule-deploy-cloudrun/get-v1-projects-goblet-locations-us-central1-services-goblet_1.json b/goblet/tests/data/http/schedule-deploy-cloudrun/get-v1-projects-goblet-locations-us-central1-services-goblet_1.json
new file mode 100644
index 00000000..0000b501
--- /dev/null
+++ b/goblet/tests/data/http/schedule-deploy-cloudrun/get-v1-projects-goblet-locations-us-central1-services-goblet_1.json
@@ -0,0 +1,10 @@
+{
+  "headers": {},
+  "body": {
+    "apiVersion": "serving.knative.dev/v1",
+    "kind": "Service",
+    "status": {
+      "url": "https://goblet-12345.a.run.app"
+    }
+  }
+}
\ No newline at end of file
diff --git a/goblet/tests/data/http/schedule-deploy-cloudrun/post-v1-projects-goblet-locations-us-central1-jobs_1.json b/goblet/tests/data/http/schedule-deploy-cloudrun/post-v1-projects-goblet-locations-us-central1-jobs_1.json
new file mode 100644
index 00000000..a4943c53
--- /dev/null
+++ b/goblet/tests/data/http/schedule-deploy-cloudrun/post-v1-projects-goblet-locations-us-central1-jobs_1.json
@@ -0,0 +1,25 @@
+{
+  "headers": {},
+  "body": {
+    "name": "projects/goblet/locations/us-central1/jobs/goblet-test-job",
+    "description": "Created by goblet",
+    "httpTarget": {
+      "uri": "https://goblet-12345.a.run.app",
+      "httpMethod": "GET",
+      "headers": {
+        "User-Agent": "Google-Cloud-Scheduler",
+        "X-Goblet-Type": "schedule",
+        "X-Goblet-Name": "test-job"
+      },
+      "oidcToken": {
+        "serviceAccountEmail": "SERVICE_ACCOUNT@developer.gserviceaccount.com",
+        "audience": "https://goblet-12345.a.run.app"
+      }
+    },
+    "userUpdateTime": "2022-01-12T15:34:18Z",
+    "state": "ENABLED",
+    "schedule": "* * * * *",
+    "timeZone": "UTC",
+    "attemptDeadline": "180s"
+  }
+}
\ No newline at end of file
diff --git a/goblet/tests/test_app.py b/goblet/tests/test_app.py
index a9c7c4b8..2b8cc710 100644
--- a/goblet/tests/test_app.py
+++ b/goblet/tests/test_app.py
@@ -50,8 +50,8 @@ def dummy_function2(self, home_id):
 
         app1 + app2
 
-        assert list(app1.handlers['route'].routes.keys()) == ['/home', '/home2']
-        assert list(app1.handlers['schedule'].jobs.keys()) == ['dummy_function', 'dummy_function2']
+        assert list(app1.handlers['route'].resources.keys()) == ['/home', '/home2']
+        assert list(app1.handlers['schedule'].resources.keys()) == ['dummy_function', 'dummy_function2']
 
     def test_combine(self):
         app1 = Goblet("test")
@@ -69,8 +69,8 @@ def dummy_function2(self, home_id):
 
         app1.combine(app2)
 
-        assert list(app1.handlers['route'].routes.keys()) == ['/home', '/home2']
-        assert list(app1.handlers['schedule'].jobs.keys()) == ['dummy_function', 'dummy_function2']
+        assert list(app1.handlers['route'].resources.keys()) == ['/home', '/home2']
+        assert list(app1.handlers['schedule'].resources.keys()) == ['dummy_function', 'dummy_function2']
 
     def test_is_http(self):
 
diff --git a/goblet/tests/test_deployer.py b/goblet/tests/test_deployer.py
index 9a668979..af217c60 100644
--- a/goblet/tests/test_deployer.py
+++ b/goblet/tests/test_deployer.py
@@ -2,6 +2,8 @@
 from goblet.resources.http import HTTP
 from goblet import Goblet
 from goblet.test_utils import get_responses, dummy_function, DATA_DIR_MAIN
+import subprocess
+from unittest.mock import Mock
 
 
 class TestDeployer:
@@ -22,6 +24,39 @@ def test_deploy_http_function(self, monkeypatch):
         responses = get_responses('deployer-function-deploy')
         assert(len(responses) == 3)
 
+    def test_deploy_cloudrun(self, monkeypatch):
+        monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
+        monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
+        monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
+
+        mock = Mock()
+
+        monkeypatch.setattr(subprocess, 'check_output', mock)
+
+        app = Goblet(function_name="goblet", backend="cloudrun")
+        setattr(app, "entrypoint", 'app')
+
+        app.handlers['http'] = HTTP(dummy_function)
+
+        Deployer({"name": app.function_name}).deploy(app, only_function=True, force=True, config={"cloudrun": {"no-allow-unauthenticated": "", "max-instances": "2"}})
+
+        assert set(['gcloud', 'run', 'deploy', "--no-allow-unauthenticated", "--max-instances", "2"]).issubset(set(mock.call_args[0][0]))
+
+    def test_destroy_cloudrun(self, monkeypatch):
+        monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
+        monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
+        monkeypatch.setenv("GOBLET_TEST_NAME", "deployer-cloudrun-destroy")
+        monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
+
+        app = Goblet(function_name="goblet", backend="cloudrun")
+
+        Deployer({"name": app.function_name}).destroy(app)
+
+        responses = get_responses('deployer-cloudrun-destroy')
+        assert(len(responses) == 1)
+        assert(responses[0]['body']['status'] == 'Success')
+        assert(responses[0]['body']['details']['name'] == "goblet")
+
     def test_destroy_http_function(self, monkeypatch):
         monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
         monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
@@ -30,7 +65,7 @@ def test_destroy_http_function(self, monkeypatch):
 
         app = Goblet(function_name="goblet_example")
 
-        Deployer().destroy(app)
+        Deployer(config={"name": "goblet_test_app"}).destroy(app)
 
         responses = get_responses('deployer-function-destroy')
         assert(len(responses) == 1)
@@ -61,7 +96,7 @@ def test_set_iam_bindings(self, monkeypatch):
 
         app.handlers['http'] = HTTP(dummy_function)
         bindings = [{"role": "roles/cloudfunctions.invoker", "members": ["allUsers"]}]
-        Deployer().deploy(app, only_function=True, config={'bindings': bindings}, force=True)
+        Deployer(config={"name": "goblet_test_app"}).deploy(app, only_function=True, config={'bindings': bindings}, force=True)
 
         responses = get_responses('deployer-function-bindings')
         assert(len(responses) == 4)
@@ -75,5 +110,5 @@ def test_cloudfunction_delta(self, monkeypatch, requests_mock):
 
         requests_mock.register_uri("HEAD", 'https://storage.googleapis.com/mock', headers={'x-goog-hash': 'crc32c=+kjoHA==, md5=QcWxCkEOHzBSBgerQcjMEg=='})
 
-        assert not Deployer()._cloudfunction_delta(f"{DATA_DIR_MAIN}/test_zip.txt.zip")
-        assert Deployer()._cloudfunction_delta(f"{DATA_DIR_MAIN}/fail_test_zip.txt.zip")
+        assert not Deployer(config={"name": "goblet_test_app"})._cloudfunction_delta(f"{DATA_DIR_MAIN}/test_zip.txt.zip")
+        assert Deployer(config={"name": "goblet_test_app"})._cloudfunction_delta(f"{DATA_DIR_MAIN}/fail_test_zip.txt.zip")
diff --git a/goblet/tests/test_pubsub.py b/goblet/tests/test_pubsub.py
index 0af57c51..11d3b61e 100644
--- a/goblet/tests/test_pubsub.py
+++ b/goblet/tests/test_pubsub.py
@@ -16,8 +16,8 @@ def test_add_topic(self):
         app.topic('test')(dummy_function)
 
         pubsub = app.handlers["pubsub"]
-        assert(len(pubsub.topics) == 1)
-        assert(pubsub.topics['test']['dummy_function'] == {'func': dummy_function, 'attributes': {}})
+        assert(len(pubsub.resources) == 1)
+        assert(pubsub.resources['test']['dummy_function'] == {'func': dummy_function, 'attributes': {}})
 
     def test_add_topic_attributes(self):
         app = Goblet(function_name="goblet_example")
@@ -25,8 +25,8 @@ def test_add_topic_attributes(self):
         app.topic('test', attributes={'test': True})(dummy_function)
 
         pubsub = app.handlers["pubsub"]
-        assert(len(pubsub.topics) == 1)
-        assert(pubsub.topics['test']['dummy_function'] == {'func': dummy_function, 'attributes': {'test': True}})
+        assert(len(pubsub.resources) == 1)
+        assert(pubsub.resources['test']['dummy_function'] == {'func': dummy_function, 'attributes': {'test': True}})
 
     def test_call_topic(self):
         app = Goblet(function_name="goblet_example")
@@ -96,7 +96,7 @@ def test_destroy_pubsub(self, monkeypatch):
         monkeypatch.setenv("GOBLET_TEST_NAME", "pubsub-destroy")
         monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
 
-        pubsub = PubSub('goblet_topic', topics={'test-topic': {}})
+        pubsub = PubSub('goblet_topic', resources={'test-topic': {}})
         pubsub.destroy()
 
         responses = get_responses('pubsub-destroy')
@@ -104,3 +104,40 @@ def test_destroy_pubsub(self, monkeypatch):
         assert(len(responses) == 1)
         assert(responses[0]['body']['metadata']['type'] == 'DELETE_FUNCTION')
         assert(responses[0]['body']['metadata']['target'].endswith('goblet_topic-topic-test-topic'))
+
+    def test_deploy_pubsub_cloudrun(self, monkeypatch):
+        monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
+        monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
+        monkeypatch.setenv("GOBLET_TEST_NAME", "pubsub-deploy-cloudrun")
+        monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
+
+        pubsub = PubSub('goblet', backend="cloudrun")
+        pubsub.register_topic('test', None, kwargs={'topic': 'test', "kwargs": {}})
+
+        cloudrun_url = "https://goblet-12345.a.run.app"
+        service_account = "SERVICE_ACCOUNT@developer.gserviceaccount.com"
+
+        pubsub._deploy(config={'pubsub': {'serviceAccountEmail': service_account}})
+
+        responses = get_responses('pubsub-deploy-cloudrun')
+
+        assert(len(responses) == 2)
+        assert(responses[0]['body']['status']['url'] == cloudrun_url)
+        assert(responses[1]['body']['pushConfig']['oidcToken']['serviceAccountEmail'] == service_account)
+        assert(responses[1]['body']['pushConfig']['oidcToken']['audience'] == cloudrun_url)
+        assert(responses[1]['body']['pushConfig']['pushEndpoint'] == cloudrun_url)
+        assert(responses[1]['body']['topic'] == "projects/goblet/topics/test")
+
+    def test_destroy_pubsub_cloudrun(self, monkeypatch):
+        monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
+        monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
+        monkeypatch.setenv("GOBLET_TEST_NAME", "pubsub-destroy-cloudrun")
+        monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
+
+        pubsub = PubSub('goblet', resources={'test': {}}, backend="cloudrun")
+        pubsub.destroy()
+
+        responses = get_responses('pubsub-destroy-cloudrun')
+
+        assert(len(responses) == 1)
+        assert(responses[0]['body'] == {})
diff --git a/goblet/tests/test_routes.py b/goblet/tests/test_routes.py
index 8076cb92..0577d8ba 100644
--- a/goblet/tests/test_routes.py
+++ b/goblet/tests/test_routes.py
@@ -13,8 +13,8 @@ def test_add_base_route(self):
         app.route('/home')(dummy_function)
 
         gateway = app.handlers["route"]
-        assert(len(gateway.routes) == 1)
-        route_entry = gateway.routes['/home']['GET']
+        assert(len(gateway.resources) == 1)
+        route_entry = gateway.resources['/home']['GET']
         assert(route_entry.method == 'GET')
         assert(route_entry.function_name == 'dummy_function')
         assert(route_entry.uri_pattern == '/home')
@@ -26,8 +26,8 @@ def test_add_route_path_params(self):
         app.route('/home/{home_id}', content_types={'home_id': 'boolean'})(dummy_function)
 
         gateway = app.handlers["route"]
-        assert(len(gateway.routes) == 1)
-        route_entry = gateway.routes['/home/{home_id}']['GET']
+        assert(len(gateway.resources) == 1)
+        route_entry = gateway.resources['/home/{home_id}']['GET']
         assert(route_entry.method == 'GET')
         assert(route_entry.function_name == 'dummy_function')
         assert(route_entry.uri_pattern == '/home/{home_id}')
@@ -44,12 +44,12 @@ def dummy_function(self):
         def dummy_function2(self):
             return True
         gateway = app.handlers["route"]
-        assert(len(gateway.routes) == 1)
-        assert(gateway.routes['/home']['GET'])
-        assert(gateway.routes['/home']['POST'])
-        assert(gateway.routes['/home']['PUT'])
-        assert(gateway.routes['/home']['GET'].route_function == gateway.routes['/home']['POST'].route_function)
-        assert(gateway.routes['/home']['GET'].route_function != gateway.routes['/home']['PUT'].route_function)
+        assert(len(gateway.resources) == 1)
+        assert(gateway.resources['/home']['GET'])
+        assert(gateway.resources['/home']['POST'])
+        assert(gateway.resources['/home']['PUT'])
+        assert(gateway.resources['/home']['GET'].route_function == gateway.resources['/home']['POST'].route_function)
+        assert(gateway.resources['/home']['GET'].route_function != gateway.resources['/home']['PUT'].route_function)
 
     def test_add_multiple_routes(self):
         app = Goblet(function_name="goblet_example")
@@ -58,9 +58,9 @@ def test_add_multiple_routes(self):
         app.route('/home2')(dummy_function)
 
         gateway = app.handlers["route"]
-        assert(len(gateway.routes) == 2)
-        assert(gateway.routes['/home']['GET'])
-        assert(gateway.routes['/home2']['GET'])
+        assert(len(gateway.resources) == 2)
+        assert(gateway.resources['/home']['GET'])
+        assert(gateway.resources['/home2']['GET'])
 
     def test_call_route(self):
         app = Goblet(function_name="goblet_example")
@@ -164,7 +164,7 @@ def test_destroy_routes(self, monkeypatch):
         monkeypatch.setenv("GOBLET_TEST_NAME", "routes-destroy")
         monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
 
-        apigw = ApiGateway('goblet_routes', routes=['not_empty'])
+        apigw = ApiGateway('goblet_routes', resources=['not_empty'])
         apigw.destroy()
 
         responses = get_responses('routes-destroy')
diff --git a/goblet/tests/test_scheduler.py b/goblet/tests/test_scheduler.py
index bc1ea39c..c0913825 100644
--- a/goblet/tests/test_scheduler.py
+++ b/goblet/tests/test_scheduler.py
@@ -14,7 +14,7 @@ def test_add_schedule(self, monkeypatch):
         app.schedule('* * * * *', description='test')(dummy_function)
 
         scheduler = app.handlers["schedule"]
-        assert(len(scheduler.jobs) == 1)
+        assert(len(scheduler.resources) == 1)
         scheule_json = {
             'name': 'projects/TEST_PROJECT/locations/us-central1/jobs/goblet_example-dummy_function',
             'schedule': '* * * * *',
@@ -32,8 +32,8 @@ def test_add_schedule(self, monkeypatch):
                 'oidcToken': {}
             }
         }
-        assert(scheduler.jobs['dummy_function']['job_json'] == scheule_json)
-        assert(scheduler.jobs['dummy_function']['func'] == dummy_function)
+        assert(scheduler.resources['dummy_function']['job_json'] == scheule_json)
+        assert(scheduler.resources['dummy_function']['func'] == dummy_function)
 
     def test_multiple_schedules(self, monkeypatch):
         app = Goblet(function_name="goblet_example")
@@ -45,7 +45,7 @@ def test_multiple_schedules(self, monkeypatch):
         app.schedule('3 * * * *', httpMethod='POST')(dummy_function)
 
         scheduler = app.handlers["schedule"]
-        assert(len(scheduler.jobs) == 3)
+        assert(len(scheduler.resources) == 3)
         scheule_json = {
             'name': 'projects/TEST_PROJECT/locations/us-central1/jobs/goblet_example-dummy_function',
             'schedule': '1 * * * *',
@@ -63,11 +63,11 @@ def test_multiple_schedules(self, monkeypatch):
                 'oidcToken': {}
             }
         }
-        assert(scheduler.jobs['dummy_function']['job_json'] == scheule_json)
-        assert(scheduler.jobs['dummy_function-2']['job_json']['httpTarget']['headers']['test'] == 'header')
-        assert(scheduler.jobs['dummy_function-2']['job_json']['httpTarget']['headers']['X-Goblet-Name'] == 'dummy_function-2')
-        assert(scheduler.jobs['dummy_function-3']['job_json']['httpTarget']['headers']['X-Goblet-Name'] == 'dummy_function-3')
-        assert(scheduler.jobs['dummy_function-3']['job_json']['httpTarget']['httpMethod'] == 'POST')
+        assert(scheduler.resources['dummy_function']['job_json'] == scheule_json)
+        assert(scheduler.resources['dummy_function-2']['job_json']['httpTarget']['headers']['test'] == 'header')
+        assert(scheduler.resources['dummy_function-2']['job_json']['httpTarget']['headers']['X-Goblet-Name'] == 'dummy_function-2')
+        assert(scheduler.resources['dummy_function-3']['job_json']['httpTarget']['headers']['X-Goblet-Name'] == 'dummy_function-3')
+        assert(scheduler.resources['dummy_function-3']['job_json']['httpTarget']['httpMethod'] == 'POST')
 
     def test_call_scheduler(self, monkeypatch):
         app = Goblet(function_name="goblet_example")
@@ -109,6 +109,25 @@ def test_deploy_schedule(self, monkeypatch):
         assert(responses[1]['body']['httpTarget']['headers']['X-Goblet-Type'] == 'schedule')
         assert(responses[1]['body']['schedule'] == '* * * * *')
 
+    def test_deploy_schedule_cloudrun(self, monkeypatch):
+        monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
+        monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
+        monkeypatch.setenv("GOBLET_TEST_NAME", "schedule-deploy-cloudrun")
+        monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
+
+        scheduler = Scheduler('goblet', backend="cloudrun")
+        cloudrun_url = "https://goblet-12345.a.run.app"
+        service_account = "SERVICE_ACCOUNT@developer.gserviceaccount.com"
+        scheduler.register_job('test-job', None, kwargs={'schedule': '* * * * *', 'kwargs': {}})
+        scheduler._deploy(config={"scheduler": {"serviceAccount": service_account}})
+
+        responses = get_responses('schedule-deploy-cloudrun')
+
+        assert(responses[0]['body']['status']['url'] == cloudrun_url)
+        assert(responses[1]['body']['httpTarget']['oidcToken']['serviceAccountEmail'] == service_account)
+        assert(responses[1]['body']['httpTarget']['oidcToken']['audience'] == cloudrun_url)
+        assert(responses[1]['body']['schedule'] == '* * * * *')
+
     def test_deploy_multiple_schedule(self, monkeypatch):
         monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
         monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
diff --git a/goblet/tests/test_storage.py b/goblet/tests/test_storage.py
index 3a31e07d..2abab259 100644
--- a/goblet/tests/test_storage.py
+++ b/goblet/tests/test_storage.py
@@ -16,9 +16,9 @@ def test_add_bucket(self, monkeypatch):
         app.storage('test2', 'archive')(dummy_function)
 
         storage = app.handlers["storage"]
-        assert(len(storage.buckets) == 2)
-        assert(storage.buckets[0]['event_type'] == 'finalize')
-        assert(storage.buckets[1]['event_type'] == 'archive')
+        assert(len(storage.resources) == 2)
+        assert(storage.resources[0]['event_type'] == 'finalize')
+        assert(storage.resources[1]['event_type'] == 'archive')
 
     def test_add_invalid_event(self):
         app = Goblet(function_name="goblet_example")
@@ -65,7 +65,7 @@ def test_destroy_storage(self, monkeypatch):
         monkeypatch.setenv("GOBLET_TEST_NAME", "storage-destroy")
         monkeypatch.setenv("GOBLET_HTTP_TEST", "REPLAY")
 
-        storage = Storage('goblet_storage', buckets=[{'bucket': 'test', 'event_type': 'finalize', 'name': 'test'}])
+        storage = Storage('goblet_storage', resources=[{'bucket': 'test', 'event_type': 'finalize', 'name': 'test'}])
         storage.destroy()
 
         responses = get_responses('storage-destroy')