Repository: goblet/goblet (language: python)

Issue:
[Deploy] Plugin to deploy apps like Fastapi
Would be nice to wrap arbitrary app's and use goblet to handle the deployment to api gateway + other resources such as scheduler/ pubsub topics. 

One way would be to expose a plugin framework that takes in an app and logic to create/ingest the openapi spec and then goblet would deploy all gcp  resources. 

one example could be with  [fastapi](https://fastapi.tiangolo.com/) and then using the [openapi generator](.openapi()) to pass through endpoints to goblet 



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index 7eeca7bc..5502f3f8 100644
--- a/.flake8
+++ b/.flake8
@@ -1,8 +1,8 @@
 [flake8]
 max-line-length = 88
-ignore = E501, W503 
+ignore = E501, W503, E266 
 #E501 line too long
 #W503 line break before binary operator
 ; exclude = .git,__pycache__,docs
 max-complexity = 15
-extend-ignore = 203
\ No newline at end of file
+extend-ignore = 203
diff --git a/README.md b/README.md
index 0504a555..55b7a75e 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,7 @@ $ curl https://api.uc.gateway.dev/home
 * vpc connector
 * redis
 * alerts
+* api gateway
 
 ## Data Typing Frameworks Supported
 
@@ -289,12 +290,10 @@ Please file any issues, bugs or feature requests as an issue on our [GitHub](htt
  &#9744; [Cloud Endpoints](https://cloud.google.com/endpoints/docs/openapi/get-started-cloud-functions) trigger \
  &#9745; [EventArc](https://cloud.google.com/eventarc/docs) trigger \
  &#9745; [Redis](https://cloud.google.com/memorystore) infrastructure \
- &#9744; [AlloyDB](https://cloud.google.com/alloydb/docs/overview) infrastructure \
- &#9744; [Cloudsql](https://cloud.google.com/sql) infrastructure \
- &#9744; [Spanner](https://cloud.google.com/spanner) infrastructure \
  &#9744; [Cloud Tracing](https://cloud.google.com/trace/docs/setup/python-ot) \
  &#9745; [Alerts](https://cloud.google.com/monitoring/alerts) \
  &#9745; [BQ Remote Functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions)
+ &#9745; Deploy API Gateway from existing openapi spec
 
 ## Want to Contribute
 
diff --git a/docs/source/infrastructures.rst b/docs/source/infrastructures.rst
index 007bc518..d44ecd86 100644
--- a/docs/source/infrastructures.rst
+++ b/docs/source/infrastructures.rst
@@ -53,3 +53,47 @@ You can reference `Connector Resource <https://cloud.google.com/vpc/docs/referen
 
 .. note::
     * In order to ensure proper configuration of the VPC Connector, the `ipCidrRange` key is required to be set within `vpcconnector` of your `config.json`.
+
+Api Gateway
+^^^^^^^^^^^^^
+.. code:: python
+
+    app = Goblet(function_name="openapi-existing")
+    
+    filename = "openapi_spec.yml"
+    app.apigateway("openapi-existing", "BACKEND_URL", filename=filename)
+
+.. code:: python 
+
+    app = Goblet(function_name="openapi-existing-dict")
+
+    openapi_dict = {
+            "swagger": "2.0",
+            "info": {
+                "title": "media-serving-service",
+                "description": "Goblet Autogenerated Spec",
+                "version": "1.0.0",
+            },
+            "schemes": ["https"],
+            "produces": ["application/json"],
+            "paths": {
+                "/": {
+                    "get": {
+                        "operationId": "get_main",
+                        "responses": {"200": {"description": "A successful response"}},
+                    }
+                }
+            },
+            "definitions": {},
+        }
+    app.apigateway("openapi-existing-from-dict", "BACKEND_URL", openapi_dict=openapi_dict)
+
+You can deploy an Api Gateway and all related resources (Api, Api Config, Gateway) with an existing openapi spec using the apigateway decorator. The decorator can take in a filename or the 
+spec as a dictionary. If you dont need to deploy any other Goblet resources you can deploy just the Api Gateway using 
+`goblet deploy -p PROJECT --skip-backend --skip-resources`
+
+By default there is a timeout on Api Gateway of 15 seconds. This can be overriden by setting `"api_gateway": {"deadline": 45}` in `config.json`. 
+
+.. note::
+    
+    * API Gateway only supports swagger 2.0. 
diff --git a/docs/source/resources.rst b/docs/source/resources.rst
index 37425d92..258a4ab2 100644
--- a/docs/source/resources.rst
+++ b/docs/source/resources.rst
@@ -109,8 +109,8 @@ instance itself. The routes work the same as with an apigateway, but you would a
 Scheduled Jobs
 ^^^^^^^^^^^^^^
 
-To deploy scheduled jobs using a cron schedule use the ``@app.schedule(...)`` decorator. The cron schedule follows the unix-cron format. 
-More information on the cron format can be found `here`_. Make sure `Cloud Scheduler`_ is enabled in your account if you want to deploy
+To deploy scheduled jobs using a cron schedule use the `@app.schedule(...)` decorator. The cron schedule follows the unix-cron format. 
+More information on the cron format can be found in the `gcp docs <https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules>`_. Make sure `Cloud Scheduler <https://cloud.google.com/scheduler>`_ is enabled in your account if you want to deploy
 scheduled jobs.
 
 Example usage:
@@ -158,9 +158,6 @@ For example:
     "X-Goblet-Type": "schedule",
     "X-Goblet-Name": FUNCTION_NAME
 
-.. _HERE: https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules
-.. _CLOUD SCHEDULER: https://cloud.google.com/scheduler
-
 PubSub
 ^^^^^^
 
diff --git a/goblet/cli.py b/goblet/cli.py
index 62f75c73..f9f7dfd4 100644
--- a/goblet/cli.py
+++ b/goblet/cli.py
@@ -217,7 +217,7 @@ def openapi(cloudfunction, stage, version):
         response = requests.post(
             "https://converter.swagger.io/api/convert", headers=headers, data=data
         )
-        with open(f"{get_g_dir()}/{app.function_name}_openapi_spec_3.yml", "w") as f:
+        with open(f"{get_g_dir()}/{app.function_name}_openapi_spec_3.yml", "w+") as f:
             f.write(response.text)
 
 
diff --git a/goblet/common_cloud_actions.py b/goblet/common_cloud_actions.py
index bba659cf..e8489ed6 100644
--- a/goblet/common_cloud_actions.py
+++ b/goblet/common_cloud_actions.py
@@ -2,6 +2,7 @@
 import logging
 import json
 from urllib.parse import quote_plus
+import base64
 from googleapiclient.errors import HttpError
 
 from goblet.config import GConfig
@@ -21,6 +22,7 @@
 log.setLevel(logging.INFO)
 
 
+####### Cloud Functions #######
 def create_cloudfunctionv1(client: Client, params: dict, config=None):
     create_cloudfunction(
         client, params, config, parent_key="location", operations_calls="operations"
@@ -93,23 +95,6 @@ def destroy_cloudfunction(client, name):
             raise e
 
 
-def destroy_cloudrun(client, name):
-    """Destroys cloudrun"""
-    try:
-        client.execute(
-            "delete",
-            parent_schema="projects/{project_id}/locations/{location_id}/services/"
-            + name,
-            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")
@@ -137,6 +122,119 @@ def destroy_cloudfunction_artifacts(name):
         )
 
 
+def get_cloudfunction_url(client, name):
+    """Get the cloudrun url"""
+    if client.version == "v1":
+        return f"https://{get_default_location()}-{get_default_project()}.cloudfunctions.net/{name}"
+    try:
+        resp = client.execute(
+            "get",
+            parent_key="name",
+            parent_schema="projects/{project_id}/locations/{location_id}/functions/"
+            + name,
+        )
+        # handle both cases: first for gcf v1 and second for v2
+        try:
+            target = resp["httpsTrigger"]["url"]
+        except KeyError:
+            target = resp["serviceConfig"]["uri"]
+        return target
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info(f"cloudfunction {name} not found")
+        else:
+            raise e
+
+
+def get_function_runtime(client, config=None):
+    """
+    Returns the proper runtime to be used in cloudfunctions
+    """
+    runtime = config.runtime or get_python_runtime()
+    required_runtime = "python37" if client.version == "v1" else "python38"
+    if int(runtime.split("python")[-1]) < int(required_runtime.split("python")[-1]):
+        raise ValueError(
+            f"Your current python runtime is {runtime}. Your backend requires a minimum of {required_runtime}"
+            f". Either upgrade python on your machine or set the 'runtime' field in config.json."
+        )
+    return runtime
+
+
+####### Cloud Run #######
+
+
+def destroy_cloudrun(client, name):
+    """Destroys cloudrun"""
+    try:
+        client.execute(
+            "delete",
+            parent_schema="projects/{project_id}/locations/{location_id}/services/"
+            + name,
+            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 deploy_cloudrun(client, req_body, name):
+    """Deploys cloud build to cloudrun"""
+    try:
+        params = {"body": req_body, "serviceId": name}
+        resp = client.execute(
+            "create",
+            parent_key="parent",
+            parent_schema="projects/"
+            + get_default_project_number()
+            + "/locations/{location_id}",
+            params=params,
+        )
+        log.info("creating cloudrun")
+    except HttpError as e:
+        if e.resp.status == 409:
+            log.info("updating cloudrun")
+            resp = client.execute(
+                "patch",
+                parent_key="name",
+                parent_schema="projects/"
+                + get_default_project_number()
+                + "/locations/{location_id}/services/"
+                + name,
+                params={"body": req_body},
+            )
+        else:
+            raise e
+    client.wait_for_operation(resp["name"], calls="projects.locations.operations")
+
+
+def get_cloudrun_url(client, name):
+    """Get the cloudrun url"""
+    try:
+        resp = client.execute(
+            "get",
+            parent_key="name",
+            parent_schema="projects/{project_id}/locations/{location_id}/services/"
+            + name,
+        )
+        # Handle both cases: cloudrun v1 and v2
+        try:
+            target = resp["status"]["url"]
+        except KeyError:
+            target = resp["uri"]
+        return target
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info(f"cloudrun {name} not found")
+        else:
+            raise e
+
+
+####### Cloud Build #######
+
+
 def create_cloudbuild(client, req_body):
     """Creates a cloudbuild based on req_body"""
     defaultProject = get_default_project()
@@ -216,80 +314,7 @@ def getCloudbuildArtifact(client, artifactName, config):
     return latestArtifact
 
 
-def deploy_cloudrun(client, req_body, name):
-    """Deploys cloud build to cloudrun"""
-    try:
-        params = {"body": req_body, "serviceId": name}
-        resp = client.execute(
-            "create",
-            parent_key="parent",
-            parent_schema="projects/"
-            + get_default_project_number()
-            + "/locations/{location_id}",
-            params=params,
-        )
-        log.info("creating cloudrun")
-    except HttpError as e:
-        if e.resp.status == 409:
-            log.info("updating cloudrun")
-            resp = client.execute(
-                "patch",
-                parent_key="name",
-                parent_schema="projects/"
-                + get_default_project_number()
-                + "/locations/{location_id}/services/"
-                + name,
-                params={"body": req_body},
-            )
-        else:
-            raise e
-    client.wait_for_operation(resp["name"], calls="projects.locations.operations")
-
-
-def get_cloudrun_url(client, name):
-    """Get the cloudrun url"""
-    try:
-        resp = client.execute(
-            "get",
-            parent_key="name",
-            parent_schema="projects/{project_id}/locations/{location_id}/services/"
-            + name,
-        )
-        # Handle both cases: cloudrun v1 and v2
-        try:
-            target = resp["status"]["url"]
-        except KeyError:
-            target = resp["uri"]
-        return target
-    except HttpError as e:
-        if e.resp.status == 404:
-            log.info(f"cloudrun {name} not found")
-        else:
-            raise e
-
-
-def get_cloudfunction_url(client, name):
-    """Get the cloudrun url"""
-    if client.version == "v1":
-        return f"https://{get_default_location()}-{get_default_project()}.cloudfunctions.net/{name}"
-    try:
-        resp = client.execute(
-            "get",
-            parent_key="name",
-            parent_schema="projects/{project_id}/locations/{location_id}/functions/"
-            + name,
-        )
-        # handle both cases: first for gcf v1 and second for v2
-        try:
-            target = resp["httpsTrigger"]["url"]
-        except KeyError:
-            target = resp["serviceConfig"]["uri"]
-        return target
-    except HttpError as e:
-        if e.resp.status == 404:
-            log.info(f"cloudfunction {name} not found")
-        else:
-            raise e
+####### Pub Sub #######
 
 
 def create_pubsub_subscription(client, sub_name, req_body):
@@ -341,6 +366,9 @@ def destroy_pubsub_subscription(client, name):
             raise e
 
 
+####### Event Arc #######
+
+
 def create_eventarc_trigger(client, trigger_name, region, req_body):
     """Creates an eventarc trigger from req_body"""
     try:
@@ -392,15 +420,143 @@ def destroy_eventarc_trigger(client, trigger_name, region):
             raise e
 
 
-def get_function_runtime(client, config=None):
-    """
-    Returns the proper runtime to be used in cloudfunctions
-    """
-    runtime = config.runtime or get_python_runtime()
-    required_runtime = "python37" if client.version == "v1" else "python38"
-    if int(runtime.split("python")[-1]) < int(required_runtime.split("python")[-1]):
-        raise ValueError(
-            f"Your current python runtime is {runtime}. Your backend requires a minimum of {required_runtime}"
-            f". Either upgrade python on your machine or set the 'runtime' field in config.json."
+####### Api Gateway #######
+
+
+def deploy_apigateway(name, gconfig, versioned_clients, spec_path):
+    "Deploy an Api, Api Gateway, & Api Config"
+    try:
+        resp = versioned_clients.apigateway_api.execute(
+            "create",
+            params={"apiId": name, "body": {"labels": gconfig.labels}},
         )
-    return runtime
+        versioned_clients.apigateway_api.wait_for_operation(resp["name"])
+    except HttpError as e:
+        if e.resp.status == 409:
+            log.info("api already deployed")
+        else:
+            raise e
+    config = {
+        "openapiDocuments": [
+            {
+                "document": {
+                    "path": spec_path,
+                    "contents": base64.b64encode(open(spec_path, "rb").read()).decode(
+                        "utf-8"
+                    ),
+                }
+            }
+        ],
+        "labels": gconfig.labels,
+        **(gconfig.apiConfig or {}),
+    }
+    try:
+        config_version_name = name
+        versioned_clients.apigateway_configs.execute(
+            "create",
+            params={"apiConfigId": name, "body": config},
+            parent_schema="projects/{project_id}/locations/global/apis/" + name,
+        )
+    except HttpError as e:
+        if e.resp.status == 409:
+            log.info("updating api endpoints")
+            configs = versioned_clients.apigateway_configs.execute(
+                "list",
+                parent_schema="projects/{project_id}/locations/global/apis/" + name,
+            )
+            # TODO: use hash
+            version = len(configs["apiConfigs"])
+            config_version_name = f"{name}-v{version}"
+            versioned_clients.apigateway_configs.execute(
+                "create",
+                parent_schema="projects/{project_id}/locations/global/apis/" + name,
+                params={"apiConfigId": config_version_name, "body": config},
+            )
+        else:
+            raise e
+    gateway = {
+        "apiConfig": f"projects/{get_default_project()}/locations/global/apis/{name}/configs/{config_version_name}",
+        "labels": gconfig.labels,
+    }
+    try:
+        gateway_resp = versioned_clients.apigateway.execute(
+            "create", params={"gatewayId": name, "body": gateway}
+        )
+    except HttpError as e:
+        if e.resp.status == 409:
+            log.info("updating gateway")
+            gateway_resp = versioned_clients.apigateway.execute(
+                "patch",
+                parent_key="name",
+                parent_schema="projects/{project_id}/locations/{location_id}/gateways/"
+                + name,
+                params={"updateMask": "apiConfig", "body": gateway},
+            )
+        else:
+            raise e
+    if gateway_resp:
+        versioned_clients.apigateway.wait_for_operation(gateway_resp["name"])
+    log.info("api successfully deployed...")
+    gateway_resp = versioned_clients.apigateway.execute(
+        "get",
+        parent_key="name",
+        parent_schema="projects/{project_id}/locations/{location_id}/gateways/" + name,
+    )
+    log.info(f"api endpoint is {gateway_resp['defaultHostname']}")
+
+
+def destroy_apigateway(name, versioned_clients):
+    """Destroy api gateway"""
+    try:
+        resp = versioned_clients.apigateway.execute(
+            "delete",
+            parent_schema="projects/{project_id}/locations/{location_id}/gateways/"
+            + name,
+            parent_key="name",
+        )
+        log.info("destroying api gateway......")
+        versioned_clients.apigateway_configs.wait_for_operation(resp["name"])
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info("api gateway already destroyed")
+        else:
+            raise e
+    # destroy api config
+    try:
+        configs = versioned_clients.apigateway_configs.execute(
+            "list",
+            parent_schema="projects/{project_id}/locations/global/apis/" + name,
+        )
+        resp = {}
+        log.info("api configs destroying....")
+        for c in configs.get("apiConfigs", []):
+            resp = versioned_clients.apigateway_configs.execute(
+                "delete",
+                parent_key="name",
+                parent_schema="projects/{project_id}/locations/global/apis/"
+                + name
+                + "/configs/"
+                + c["displayName"],
+            )
+            if resp:
+                versioned_clients.apigateway_configs.wait_for_operation(resp["name"])
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info("api configs already destroyed")
+        else:
+            raise e
+
+    # destroy api
+    try:
+        resp = versioned_clients.apigateway_api.execute(
+            "delete",
+            parent_key="name",
+            parent_schema="projects/{project_id}/locations/global/apis/" + name,
+        )
+        versioned_clients.apigateway_configs.wait_for_operation(resp["name"])
+        log.info("apis successfully destroyed......")
+    except HttpError as e:
+        if e.resp.status == 404:
+            log.info("api already destroyed")
+        else:
+            raise e
diff --git a/goblet/decorators.py b/goblet/decorators.py
index ea502b23..036f5f38 100644
--- a/goblet/decorators.py
+++ b/goblet/decorators.py
@@ -1,28 +1,31 @@
 from __future__ import annotations
 import os
+import yaml
+from warnings import warn
+import logging
+from googleapiclient.errors import HttpError
+
+from goblet.client import VersionedClients
+from goblet_gcp_client.client import get_default_location, get_default_project
 
 from goblet.backends.cloudfunctionv1 import CloudFunctionV1
 from goblet.backends.cloudfunctionv2 import CloudFunctionV2
 from goblet.backends.cloudrun import CloudRun
-from goblet.client import VersionedClients
-from goblet_gcp_client.client import get_default_location, get_default_project
-from goblet.infrastructures.redis import Redis
-from goblet.infrastructures.vpcconnector import VPCConnector
+
 from goblet.resources.bq_remote_function import BigQueryRemoteFunction
 from goblet.resources.eventarc import EventArc
 from goblet.resources.pubsub import PubSub
-from goblet.resources.routes import ApiGateway
+from goblet.resources.routes import Routes
 from goblet.resources.scheduler import Scheduler
 from goblet.resources.storage import Storage
 from goblet.resources.http import HTTP
 from goblet.resources.jobs import Jobs
-from goblet.infrastructures.alerts import Alerts
 
-from googleapiclient.errors import HttpError
-
-from warnings import warn
+from goblet.infrastructures.redis import Redis
+from goblet.infrastructures.vpcconnector import VPCConnector
+from goblet.infrastructures.alerts import Alerts
+from goblet.infrastructures.apigateway import ApiGateway
 
-import logging
 
 log = logging.getLogger(__name__)
 log.setLevel(logging.INFO)
@@ -182,6 +185,32 @@ def job(self, name, task_id=0, schedule=None, timezone="UTC", **kwargs):
             registration_kwargs={"name": name, "task_id": task_id, "kwargs": kwargs},
         )
 
+    def apigateway(self, name, backend_url, filename=None, openapi_dict=None, **kwargs):
+        """Api Gateway Infrastructure with an existing openapi spec.
+        Requires either a filename or openapi_dict"""
+        if not filename and not openapi_dict:
+            raise ValueError(
+                "Either a filename or the openapi_dict needs to be provided"
+            )
+        if filename and openapi_dict:
+            raise ValueError(
+                "Only one of either a filename or the openapi_dict needs to be provided"
+            )
+        if filename:
+            with open(filename) as f:
+                openapi_dict = yaml.safe_load(f.read())
+        return self._register_infrastructure(
+            handler_type="apigateway",
+            kwargs={
+                "name": name,
+                "kwargs": {
+                    "backend_url": backend_url,
+                    "openapi_dict": openapi_dict,
+                    **kwargs,
+                },
+            },
+        )
+
     def alert(self, name, conditions, **kwargs):
         """Alert Resource"""
         kwargs["conditions"] = conditions
@@ -258,7 +287,7 @@ def __init__(
         versioned_clients = VersionedClients(client_versions or {})
 
         self.handlers = {
-            "route": ApiGateway(
+            "route": Routes(
                 function_name,
                 cors=cors,
                 backend=backend,
@@ -307,6 +336,12 @@ def __init__(
                 versioned_clients=versioned_clients,
                 config=config,
             ),
+            "apigateway": ApiGateway(
+                function_name,
+                backend=backend,
+                versioned_clients=versioned_clients,
+                config=config,
+            ),
         }
 
         self.middleware_handlers = {
diff --git a/goblet/infrastructures/alerts.py b/goblet/infrastructures/alerts.py
index 7b4a9afc..b890fad5 100644
--- a/goblet/infrastructures/alerts.py
+++ b/goblet/infrastructures/alerts.py
@@ -47,9 +47,6 @@ def gcp_deployed_alerts(self):
                 self._gcp_deployed_alerts[alert["displayName"]] = alert
         return self._gcp_deployed_alerts
 
-    def get_config(self):
-        return None
-
     def deploy(self, source=None, entrypoint=None, config={}):
         if not self.resource:
             return
diff --git a/goblet/infrastructures/apigateway.py b/goblet/infrastructures/apigateway.py
new file mode 100644
index 00000000..ead825c7
--- /dev/null
+++ b/goblet/infrastructures/apigateway.py
@@ -0,0 +1,51 @@
+import logging
+import os
+
+from goblet.common_cloud_actions import deploy_apigateway, destroy_apigateway
+from goblet.infrastructures.infrastructure import Infrastructure
+from goblet.resources.routes import OpenApiSpec
+from goblet.utils import get_g_dir, get_dir
+
+log = logging.getLogger("goblet.deployer")
+log.setLevel(logging.INFO)
+
+
+class ApiGateway(Infrastructure):
+    """Api Gateway that is deployed with an existing openapi spec"""
+
+    resource_type = "apigateway"
+
+    def register(self, name, **kwargs):
+        kwargs = kwargs["kwargs"]
+        self.resource = {
+            "name": name,
+            "backend_url": kwargs["backend_url"],
+            "openapi_dict": kwargs["openapi_dict"],
+        }
+
+    def deploy(self, config={}):
+        if not self.resource:
+            return
+        self.config.update_g_config(values=config)
+        goblet_spec = OpenApiSpec(
+            self.resource["name"],
+            self.resource["backend_url"],
+            existing_spec=self.resource["openapi_dict"],
+        )
+        goblet_spec.add_x_google_backend()
+
+        # create .goblet if doesnt exist
+        if not os.path.isdir(f"{get_dir()}/.goblet"):
+            os.mkdir(f"{get_dir()}/.goblet")
+
+        updated_filename = f"{get_g_dir()}/{self.resource['name']}_openapi_spec.yml"
+        with open(updated_filename, "w") as f:
+            goblet_spec.write(f)
+        deploy_apigateway(
+            self.resource["name"], self.config, self.client, updated_filename
+        )
+
+    def destroy(self):
+        if not self.resource:
+            return
+        destroy_apigateway(self.resource["name"], self.client)
diff --git a/goblet/infrastructures/infrastructure.py b/goblet/infrastructures/infrastructure.py
index 2f34be54..e6776b4e 100644
--- a/goblet/infrastructures/infrastructure.py
+++ b/goblet/infrastructures/infrastructure.py
@@ -39,4 +39,4 @@ def _sync(self, dryrun=False):
         pass
 
     def get_config(self, config={}):
-        raise NotImplementedError("get_config")
+        return None
diff --git a/goblet/resources/routes.py b/goblet/resources/routes.py
index cd6bb480..aff5988e 100644
--- a/goblet/resources/routes.py
+++ b/goblet/resources/routes.py
@@ -1,32 +1,30 @@
 from collections import OrderedDict
 from marshmallow.schema import Schema
 from pydantic import BaseModel
-import base64
 import logging
 import re
 from typing import get_type_hints
 from apispec import APISpec
 from apispec.ext.marshmallow import MarshmallowPlugin
-from googleapiclient.errors import HttpError
 
 import goblet
 
 from goblet.resources.handler import Handler
-from goblet.client import get_default_project
 from goblet.resources.plugins.pydantic import PydanticPlugin
 from goblet.utils import get_g_dir
 from goblet.config import GConfig
+from goblet.common_cloud_actions import deploy_apigateway, destroy_apigateway
 
 log = logging.getLogger("goblet.deployer")
 log.setLevel(logging.INFO)
 
 
-class ApiGateway(Handler):
-    """Api Gateway instance, which includes api, api config, api gateway instances
+class Routes(Handler):
+    """Either cloudrun routes type or Api Gateway instance, which includes api, api config, api gateway instances
     https://cloud.google.com/api-gateway
     """
 
-    resource_type = "apigateway"
+    resource_type = "routes"
     valid_backends = ["cloudfunction", "cloudrun", "cloudfunctionv2"]
 
     def __init__(
@@ -38,7 +36,7 @@ def __init__(
         resources=None,
         routes_type="apigateway",
     ):
-        super(ApiGateway, self).__init__(
+        super(Routes, self).__init__(
             name=name,
             versioned_clients=versioned_clients,
             resources=resources,
@@ -120,153 +118,18 @@ def _deploy(self, source=None, entrypoint=None, config={}):
         log.info("deploying api......")
         base_url = self.backend.http_endpoint
         self.generate_openapi_spec(base_url)
-        try:
-            resp = self.versioned_clients.apigateway_api.execute(
-                "create",
-                params={"apiId": self.name, "body": {"labels": gconfig.labels}},
-            )
-            self.versioned_clients.apigateway_api.wait_for_operation(resp["name"])
-        except HttpError as e:
-            if e.resp.status == 409:
-                log.info("api already deployed")
-            else:
-                raise e
-        config = {
-            "openapiDocuments": [
-                {
-                    "document": {
-                        "path": f"{get_g_dir()}/{self.name}_openapi_spec.yml",
-                        "contents": base64.b64encode(
-                            open(
-                                f"{get_g_dir()}/{self.name}_openapi_spec.yml", "rb"
-                            ).read()
-                        ).decode("utf-8"),
-                    }
-                }
-            ],
-            "labels": gconfig.labels,
-            **(gconfig.apiConfig or {}),
-        }
-        try:
-            config_version_name = self.name
-            self.versioned_clients.apigateway_configs.execute(
-                "create",
-                params={"apiConfigId": self.name, "body": config},
-                parent_schema="projects/{project_id}/locations/global/apis/"
-                + self.name,
-            )
-        except HttpError as e:
-            if e.resp.status == 409:
-                log.info("updating api endpoints")
-                configs = self.versioned_clients.apigateway_configs.execute(
-                    "list",
-                    parent_schema="projects/{project_id}/locations/global/apis/"
-                    + self.name,
-                )
-                # TODO: use hash
-                version = len(configs["apiConfigs"])
-                config_version_name = f"{self.name}-v{version}"
-                self.versioned_clients.apigateway_configs.execute(
-                    "create",
-                    parent_schema="projects/{project_id}/locations/global/apis/"
-                    + self.name,
-                    params={"apiConfigId": config_version_name, "body": config},
-                )
-            else:
-                raise e
-        gateway = {
-            "apiConfig": f"projects/{get_default_project()}/locations/global/apis/{self.name}/configs/{config_version_name}",
-            "labels": gconfig.labels,
-        }
-        try:
-            gateway_resp = self.versioned_clients.apigateway.execute(
-                "create", params={"gatewayId": self.name, "body": gateway}
-            )
-        except HttpError as e:
-            if e.resp.status == 409:
-                log.info("updating gateway")
-                gateway_resp = self.versioned_clients.apigateway.execute(
-                    "patch",
-                    parent_key="name",
-                    parent_schema="projects/{project_id}/locations/{location_id}/gateways/"
-                    + self.name,
-                    params={"updateMask": "apiConfig", "body": gateway},
-                )
-            else:
-                raise e
-        if gateway_resp:
-            self.versioned_clients.apigateway.wait_for_operation(gateway_resp["name"])
-        log.info("api successfully deployed...")
-        gateway_resp = self.versioned_clients.apigateway.execute(
-            "get",
-            parent_key="name",
-            parent_schema="projects/{project_id}/locations/{location_id}/gateways/"
-            + self.name,
+        deploy_apigateway(
+            self.name,
+            gconfig,
+            self.versioned_clients,
+            f"{get_g_dir()}/{self.name}_openapi_spec.yml",
         )
-        log.info(f"api endpoint is {gateway_resp['defaultHostname']}")
         return
 
     def destroy(self):
         if len(self.resources) == 0 or self.routes_type != "apigateway":
             return
-        # destroy api gateway
-        try:
-            resp = self.versioned_clients.apigateway.execute(
-                "delete",
-                parent_schema="projects/{project_id}/locations/{location_id}/gateways/"
-                + self.name,
-                parent_key="name",
-            )
-            log.info("destroying api gateway......")
-            self.versioned_clients.apigateway_configs.wait_for_operation(resp["name"])
-        except HttpError as e:
-            if e.resp.status == 404:
-                log.info("api gateway already destroyed")
-            else:
-                raise e
-        # destroy api config
-        try:
-            configs = self.versioned_clients.apigateway_configs.execute(
-                "list",
-                parent_schema="projects/{project_id}/locations/global/apis/"
-                + self.name,
-            )
-            resp = {}
-            log.info("api configs destroying....")
-            for c in configs.get("apiConfigs", []):
-                resp = self.versioned_clients.apigateway_configs.execute(
-                    "delete",
-                    parent_key="name",
-                    parent_schema="projects/{project_id}/locations/global/apis/"
-                    + self.name
-                    + "/configs/"
-                    + c["displayName"],
-                )
-                if resp:
-                    self.versioned_clients.apigateway_configs.wait_for_operation(
-                        resp["name"]
-                    )
-        except HttpError as e:
-            if e.resp.status == 404:
-                log.info("api configs already destroyed")
-            else:
-                raise e
-
-        # destroy api
-        try:
-            resp = self.versioned_clients.apigateway_api.execute(
-                "delete",
-                parent_key="name",
-                parent_schema="projects/{project_id}/locations/global/apis/"
-                + self.name,
-            )
-            self.versioned_clients.apigateway_configs.wait_for_operation(resp["name"])
-            log.info("apis successfully destroyed......")
-        except HttpError as e:
-            if e.resp.status == 404:
-                log.info("api already destroyed")
-            else:
-                raise e
+        destroy_apigateway(self.name, self.versioned_clients)
 
     def generate_openapi_spec(self, cloudfunction):
         config = GConfig()
@@ -313,6 +176,7 @@ def __init__(
         security=None,
         marshmallow_attribute_function=None,
         deadline=15,
+        existing_spec=None,
     ):
         self.options = OrderedDict()
         self.app_name = app_name
@@ -329,14 +193,25 @@ def __init__(
         self.options["produces"] = ["application/json"]
         marshmallow_plugin = MarshmallowPlugin()
         pydantic_plugin = PydanticPlugin()
-        self.component_spec = APISpec(
-            title=self.app_name,
-            version=self.version,
-            openapi_version="2.0",
-            # Pydantic plugin needs to go first
-            plugins=[pydantic_plugin, marshmallow_plugin],
-            **self.options,
-        )
+        # Support existing spec. Needs to be version "2.0"
+        if existing_spec:
+            if not existing_spec.get("swagger") == "2.0":
+                raise ValueError("API Gateway only supports swagger 2.0")
+            self.component_spec = APISpec(
+                title=self.app_name,
+                version=self.version,
+                openapi_version="2.0",
+                **existing_spec,
+            )
+        else:
+            self.component_spec = APISpec(
+                title=self.app_name,
+                version=self.version,
+                openapi_version="2.0",
+                # Pydantic plugin needs to go first
+                plugins=[pydantic_plugin, marshmallow_plugin],
+                **self.options,
+            )
         if marshmallow_attribute_function:
             marshmallow_plugin.converter.add_attribute_function(
                 marshmallow_attribute_function
@@ -460,6 +335,17 @@ def _extract_content(self, return_type):
             param_type = self.get_param_type(return_type)
             return {"schema": {**param_type}}
 
+    def add_x_google_backend(self):
+        """Add x-google-backend section to custom openapi specs"""
+        for path in self.component_spec.options.get("paths", {}).values():
+            for method_options in path.values():
+                method_options["x-google-backend"] = {
+                    "address": self.cloudfunction,
+                    "deadline": self.deadline,
+                    "path_translation": "APPEND_PATH_TO_ADDRESS",
+                    "protocol": "h2",
+                }
+
     def write(self, file):
         file.write(self.component_spec.to_yaml())
 
@@ -523,7 +409,6 @@ def _extract_view_args(self, path):
         return args
 
     def __call__(self, request):
-        # TODO: pass in args and kwargs and options
         args = self._extract_view_args(request.path)
         resp = self.route_function(**args)
         return self._apply_cors(resp)


Test patch:
diff --git a/goblet/tests/test_apigateway.py b/goblet/tests/test_apigateway.py
new file mode 100644
index 00000000..8d1b8b26
--- /dev/null
+++ b/goblet/tests/test_apigateway.py
@@ -0,0 +1,92 @@
+import pytest
+from goblet import Goblet
+from goblet.config import GConfig
+from goblet.resources.routes import Routes
+
+from goblet.backends import CloudFunctionV1, CloudFunctionV2, CloudRun
+from goblet_gcp_client import get_replay_count
+from goblet_gcp_client.http_files import reset_replay_count
+
+
+class TestApiGatewayConfig:
+    def test_path_param_matching(self):
+        gw = Routes("test", backend=CloudFunctionV1(Goblet()))
+        assert gw._matched_path("/home/{home_id}", "/home/5")
+        assert not gw._matched_path("/home/{home_id}", "/home/5/fail")
+
+    def test_deadline(self):
+        gw = Routes("test", backend=CloudRun(Goblet(backend="cloudrun")))
+        assert gw.get_timeout(GConfig({"cloudrun_revision": {"timeout": 300}})) == 300
+        assert gw.get_timeout(GConfig()) == 15
+
+        gw = Routes("test", backend=CloudFunctionV1(Goblet()))
+        assert gw.get_timeout(GConfig({"cloudfunction": {"timeout": 300}})) == 300
+        assert gw.get_timeout(GConfig()) == 15
+
+        gw = Routes("test", backend=CloudFunctionV2(Goblet()))
+        assert (
+            gw.get_timeout(
+                GConfig({"cloudfunction": {"serviceConfig": {"timeoutSeconds": 300}}})
+            )
+            == 300
+        )
+        assert gw.get_timeout(GConfig()) == 15
+
+        gw = Routes("test", backend=CloudFunctionV1(Goblet()))
+        assert (
+            gw.get_timeout(
+                GConfig(
+                    {
+                        "cloudfunction": {"timeout": 300},
+                        "api_gateway": {"deadline": 200},
+                    }
+                )
+            )
+            == 200
+        )
+
+
+class TestApiGatewayExisting:
+    def test_invalid_inputs(self):
+        with pytest.raises(ValueError):
+            Goblet("test").apigateway("test", "URL")
+
+        with pytest.raises(ValueError):
+            Goblet("test").apigateway(
+                "test", "URL", filename="xx", openapi_dict={"x": "x"}
+            )
+
+    def test_deploy_api_gateway(self, monkeypatch, requests_mock):
+        monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
+        monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
+        monkeypatch.setenv("G_TEST_NAME", "routes-deploy")
+        monkeypatch.setenv("G_HTTP_TEST", "REPLAY")
+
+        requests_mock.register_uri("PUT", "https://storage.googleapis.com/mock")
+        reset_replay_count()
+
+        openapi_dict = {
+            "swagger": "2.0",
+            "info": {
+                "title": "media-serving-service",
+                "description": "Goblet Autogenerated Spec",
+                "version": "1.0.0",
+            },
+            "schemes": ["https"],
+            "produces": ["application/json"],
+            "paths": {
+                "/": {
+                    "get": {
+                        "operationId": "get_main",
+                        "responses": {"200": {"description": "A successful response"}},
+                    }
+                }
+            },
+            "definitions": {},
+        }
+
+        app = Goblet("goblet_routes")
+        app.apigateway("goblet-routes", "URL", openapi_dict=openapi_dict)
+        app.deploy(force=True, skip_resources=True, skip_backend=True)
+
+        assert get_replay_count() == 7
diff --git a/goblet/tests/test_routes.py b/goblet/tests/test_routes.py
index 4de8dcf8..ee674fd2 100644
--- a/goblet/tests/test_routes.py
+++ b/goblet/tests/test_routes.py
@@ -1,13 +1,12 @@
 from unittest.mock import Mock
 import pytest
 from goblet import Goblet, Response, jsonify
-from goblet.config import GConfig
-from goblet.resources.routes import ApiGateway, CORSConfig
+from goblet.resources.routes import Routes, CORSConfig
 from goblet.test_utils import (
     dummy_function,
     mock_dummy_function,
 )
-from goblet.backends import CloudFunctionV1, CloudFunctionV2, CloudRun
+from goblet.backends import CloudFunctionV1, CloudRun
 from goblet_gcp_client import get_response, get_responses
 
 
@@ -221,7 +220,7 @@ def test_deploy_routes_type_cloudrun(self, monkeypatch):
         monkeypatch.setenv("GOOGLE_LOCATION", "us-central1")
         monkeypatch.setenv("G_HTTP_TEST", "REPLAY")
 
-        gw = ApiGateway(
+        gw = Routes(
             name="test",
             routes_type="cloudrun",
             resources=[{}, {}],
@@ -231,14 +230,14 @@ def test_deploy_routes_type_cloudrun(self, monkeypatch):
         assert gw.deploy() is None
 
     def test_deploy_routes_type_cloudrun_with_incorrect_backend(self, monkeypatch):
-        ApiGateway(
+        Routes(
             name="test",
             routes_type="cloudrun",
             resources=[{}, {}],
             backend=CloudRun(Goblet(function_name="test", backend="cloudrun")),
         )
         with pytest.raises(Exception):
-            ApiGateway.deploy()
+            Routes.deploy()
 
     def test_destroy_routes(self, monkeypatch):
         monkeypatch.setenv("GOOGLE_PROJECT", "goblet")
@@ -246,7 +245,7 @@ def test_destroy_routes(self, monkeypatch):
         monkeypatch.setenv("G_TEST_NAME", "routes-destroy")
         monkeypatch.setenv("G_HTTP_TEST", "REPLAY")
 
-        apigw = ApiGateway(
+        apigw = Routes(
             "goblet_routes",
             resources=["not_empty"],
             backend=CloudFunctionV1(Goblet(function_name="goblet_routes")),
@@ -261,41 +260,3 @@ def test_destroy_routes(self, monkeypatch):
         assert responses[1]["body"]["metadata"]["target"].endswith("goblet-routes")
         assert responses[2]["body"]["metadata"]["verb"] == "delete"
         assert responses[2]["body"]["metadata"]["target"].endswith("goblet-routes")
-
-
-class TestApiGateway:
-    def test_path_param_matching(self):
-        gw = ApiGateway("test", backend=CloudFunctionV1(Goblet()))
-        assert gw._matched_path("/home/{home_id}", "/home/5")
-        assert not gw._matched_path("/home/{home_id}", "/home/5/fail")
-
-    def test_deadline(self):
-        gw = ApiGateway("test", backend=CloudRun(Goblet(backend="cloudrun")))
-        assert gw.get_timeout(GConfig({"cloudrun_revision": {"timeout": 300}})) == 300
-        assert gw.get_timeout(GConfig()) == 15
-
-        gw = ApiGateway("test", backend=CloudFunctionV1(Goblet()))
-        assert gw.get_timeout(GConfig({"cloudfunction": {"timeout": 300}})) == 300
-        assert gw.get_timeout(GConfig()) == 15
-
-        gw = ApiGateway("test", backend=CloudFunctionV2(Goblet()))
-        assert (
-            gw.get_timeout(
-                GConfig({"cloudfunction": {"serviceConfig": {"timeoutSeconds": 300}}})
-            )
-            == 300
-        )
-        assert gw.get_timeout(GConfig()) == 15
-
-        gw = ApiGateway("test", backend=CloudFunctionV1(Goblet()))
-        assert (
-            gw.get_timeout(
-                GConfig(
-                    {
-                        "cloudfunction": {"timeout": 300},
-                        "api_gateway": {"deadline": 200},
-                    }
-                )
-            )
-            == 200
-        )