def CleanUpOldResources(organization): """Cleans up deployments, proxies, and environments more than 12 hours old. This function is intended to be called as a pre-test environment cleaner, and thus will avoid adding to the flakiness potential of subsequent tests, even when doing so risks ignoring important errors. If one attempt to remove an old resource fails, no matter the reason, it will continue to try to remove the others and exit without raising any exceptions. Args: organization: the name of the organization whose resources should be cleaned up. """ identifiers = {"organizationsId": organization} try: deployments = request.ResponseToApiRequest( identifiers, ["organization"], "deployment") if "deployments" in deployments: for deployment in deployments["deployments"]: try: if _IsTimestampStale(deployment["deployStartTime"]): # More than 12 hours old. Its associated test certainly couldn't be # running anymore. deployment_identifiers = { "organizationsId": organization, "environmentsId": deployment["environment"], "apisId": deployment["apiProxy"], "revisionsId": deployment["revision"] } request.ResponseToApiRequest( deployment_identifiers, ["organization", "environment", "api", "revision"], "deployment", method="DELETE") except Exception: # pylint: disable=broad-except # Even if this deployment is broken or malformed somehow, don't let # that prevent cleanup of the others. continue except Exception: # pylint: disable=broad-except pass try: apis = request.ResponseToApiRequest(identifiers, ["organization"], "api") if "proxies" in apis: for api in apis["proxies"]: try: revision_identifiers = identifiers.copy() revision_identifiers["apisId"] = api["name"] api = request.ResponseToApiRequest( revision_identifiers, ["organization", "api"]) if _IsTimestampStale(api["metaData"]["createdAt"]): request.ResponseToApiRequest( revision_identifiers, ["organization", "api"], method="DELETE") except Exception: # pylint: disable=broad-except # Even if this API is broken or malformed somehow, don't let that # prevent cleanup of the others. continue except Exception: # pylint: disable=broad-except pass
def Revision(organization, api_proxy, revision, message=None, basepath_suffix=None): """Creates a temporary Apigee API proxy revision. The revision will have a basepath of /proxy_name/suffix, and will be automatically cleaned up upon exiting the context. Args: organization: the Apigee organization in which to create the API proxy. api_proxy: the name of the existing API proxy to which the revision should be added. message: the message the API proxy should return when called. basepath_suffix: a suffix to add to the API proxy's basepath. Yields: the revision number of the created revision. """ basepath = "/" + api_proxy if basepath_suffix: basepath += "/" + basepath_suffix query_string = urllib.parse.urlencode({"user": message}) if message else "" url_tuple = ("https", "mocktarget.apigee.net", "/user", "", query_string, "") target_url = urllib.parse.urlunparse(url_tuple) with _APIProxyArchive(api_proxy, revision, basepath, target_url) as archive: identifiers = { "organizationsId": organization, "apisId": api_proxy, "revisionsId": six.text_type(revision) } request.ResponseToApiRequest(identifiers, ["organization"], "api", method="POST", body_mimetype="application/octet-stream", body=archive, query_params={ "name": api_proxy, "action": "import" }) try: yield revision finally: request.ResponseToApiRequest(identifiers, ["organization", "api", "revision"], method="DELETE")
def List(cls, identifiers=None): if cls._entity_path is None: raise NotImplementedError("%s class must provide an entity path." % cls) return request.ResponseToApiRequest(identifiers or {}, cls._entity_path[:-1], cls._entity_path[-1])
def CreateArchiveDeployment(cls, identifiers, post_data): """Apigee API for creating a new archive deployment. Args: identifiers: A dict of identifiers for the request entity path, which must include "organizationsId" and "environmentsId". post_data: A dict of the request body to include in the CreateArchiveDeployment API call. Returns: A dict of the API response. The API call starts a long-running operation, so the response dict will contain info about the operation id. Raises: command_lib.apigee.errors.RequestError if there is an error with the API request. """ try: # The API call doesn't need to specify an archiveDeployment resource name # so only the "organizations/environments" entity path is needed. # "archive_deployment" is provided as the entity_collection argument. return request.ResponseToApiRequest(identifiers, cls._entity_path[:-1], cls._entity_path[-1], method="POST", body=json.dumps(post_data)) except errors.RequestError as error: raise error.RewrittenError("archive deployment", "create")
def GetUploadUrl(cls, identifiers): """Apigee API for generating a signed URL for uploading archives. This API uses the custom method: organizations/*/environments/*/archiveDeployments:generateUploadUrl Args: identifiers: Dict of identifiers for the request entity path, which must include "organizationsId" and "environmentsId". Returns: A dict of the API response in the form of: {"uploadUri": "https://storage.googleapis.com/ ... (full URI)"} Raises: command_lib.apigee.errors.RequestError if there is an error with the API request. """ try: # The API call doesn't need to specify an archiveDeployment resource id, # so only the "organizations/environments" entity path is needed. # "archiveDeployment" is provided as the entity_collection argument. return request.ResponseToApiRequest( identifiers, entity_path=cls._entity_path[:-1], entity_collection=cls._entity_path[-1], method=":generateUploadUrl") except errors.RequestError as error: raise error.RewrittenError("archive deployment", "get upload url for")
def Delete(cls, identifiers=None): if cls._entity_path is None: raise NotImplementedError("%s class must provide an entity path." % cls) return request.ResponseToApiRequest(identifiers or {}, cls._entity_path, method="DELETE")
def testNonstandardEndpont(self): test_data = ["hello", "world"] properties.VALUES.api_endpoint_overrides.apigee.Set( "https://api.enterprise.apigee.com/") self.AddHTTPResponse( "https://api.enterprise.apigee.com/v1/organizations", body=json.dumps(test_data)) response = request.ResponseToApiRequest({}, [], "organization") self.assertEqual(test_data, response)
def Undeploy(cls, identifiers): try: return request.ResponseToApiRequest( identifiers, ["organization", "environment", "api", "revision"], "deployment", method="DELETE") except errors.RequestError as error: # Rewrite error message to better describe what was attempted. raise error.RewrittenError("deployment", "undeploy")
def testExactObject(self): test_data = {"what": ["a", "B", 3], "test": "testExactObject"} self.AddHTTPResponse( "https://apigee.googleapis.com/v1/environments/a/apis/b", body=json.dumps(test_data)) response = request.ResponseToApiRequest(self._sample_identifiers, ["environment", "api"]) self.assertEqual(test_data, response, "Must receive the same data structure sent in response.")
def Update(cls, identifiers, product_info): product_dict = product_info._asdict() # Don't send fields unless there's a value for them. product_dict = { key: product_dict[key] for key in product_dict if product_dict[key] is not None } return request.ResponseToApiRequest(identifiers, ["organization", "product"], method="PUT", body=json.dumps(product_dict))
def APIProxy(organization, name_prefix, message=None, basepath_suffix=None): """Creates a temporary Apigee API proxy. The API proxy will have a basepath of /proxy_name/suffix, and will be automatically cleaned up upon exiting the context. Args: organization: the Apigee organization in which to create the API proxy. name_prefix: a string to include at the beginning of the API proxy's name. message: the message the API proxy should return when called. basepath_suffix: a suffix to add to the API proxy's basepath. Yields: the name of the created API proxy. """ name = next(e2e_utils.GetResourceNameGenerator(name_prefix)) basepath = "/" + name + ("/" + basepath_suffix if basepath_suffix else "") query_string = urllib.parse.urlencode({"user": message}) if message else "" url_tuple = ("https", "mocktarget.apigee.net", "/user", "", query_string, "") target_url = urllib.parse.urlunparse(url_tuple) with _APIProxyArchive(name, 1, basepath, target_url) as archive: identifiers = {"organizationsId": organization} request.ResponseToApiRequest(identifiers, ["organization"], "api", method="POST", body_mimetype="application/octet-stream", body=archive, query_params={ "name": name, "action": "import" }) try: yield name finally: identifiers["apisId"] = name request.ResponseToApiRequest(identifiers, ["organization", "api"], method="DELETE")
def testOtherResponseFormats(self): test_data = {"what": ["a", "B", 3], "test": "testOtherResponseFormats"} self.AddHTTPResponse( "https://apigee.googleapis.com/v1/environments/a/apis", body=json.dumps(test_data)[3:], request_headers={"Accept": "application/json"}) response = request.ResponseToApiRequest( self._sample_identifiers, ["environment"], "api", accept_mimetype="application/json") self.assertEqual( json.dumps(test_data)[3:], response, "Must receive the same binary data sent in response.")
def Deploy(cls, identifiers, override=False): deployment_path = ["organization", "environment", "api", "revision"] query_params = {"override": "true"} if override else {} try: return request.ResponseToApiRequest(identifiers, deployment_path, "deployment", method="POST", query_params=query_params) except errors.RequestError as error: # Rewrite error message to better describe what was attempted. raise error.RewrittenError("API proxy", "deploy")
def testRequestBody(self): test_data = {"what": ["a", "B", 3], "test": "testRequestBody"} self.AddHTTPResponse( "https://apigee.googleapis.com/v1/environments/a/apis", body=json.dumps(test_data), expected_body="abadede", request_headers={"Content-Type": "text/plain"}) response = request.ResponseToApiRequest( self._sample_identifiers, ["environment"], "api", body="abadede", body_mimetype="text/plain") self.assertEqual(test_data, response, "Must receive the same data structure sent in response.")
def testQueryParameters(self): test_data = {"what": ["a", "B", 3], "test": "testOtherResponseFormats"} params = {"verbose": "true", "another": "3"} param_values = {key: [params[key]] for key in params} self.AddHTTPResponse( "https://apigee.googleapis.com/v1/environments/a/apis", expected_params=param_values, body=json.dumps(test_data)) response = request.ResponseToApiRequest({"environmentsId": "a"}, ["environment"], "api", query_params=params) self.assertEqual(test_data, response, "Must receive the same data structure sent in response.")
def List(cls, identifiers): """Returns a list of deployments, filtered by `identifiers`. The deployment-listing API, unlike most GCP APIs, is very flexible as to what kinds of objects are provided as the deployments' parents. An organization is required, but any combination of environment, proxy or shared flow, and API revision can be given in addition to that. Args: identifiers: dictionary with fields that describe which deployments to list. `organizationsId` is required. `environmentsId`, `apisId`, and `revisionsId` can be optionally provided to further filter the list. Shared flows are not yet supported. Returns: A list of Apigee deployments, each represented by a parsed JSON object. """ identifier_names = ["organization", "environment", "api", "revision"] entities = [resource_args.ENTITIES[name] for name in identifier_names] entity_path = [] for entity in entities: key = entity.plural + "Id" if key in identifiers and identifiers[key] is not None: entity_path.append(entity.singular) if "revision" in entity_path and "api" not in entity_path: # Revision is notioinally a part of API proxy and can't be specified # without it. Behave as though neither API proxy nor revision were given. entity_path.remove("revision") try: response = request.ResponseToApiRequest(identifiers, entity_path, "deployment") except errors.EntityNotFoundError: # If there were no matches, that's just an empty list of matches. response = [] # The different endpoints this method can hit return different formats. # Translate them all into a single format. if "apiProxy" in response: return [response] if "deployments" in response: return response["deployments"] if not response: return [] return response
def List(cls, identifiers): """Calls the 'list' API for archive deployments. Args: identifiers: Dict of identifiers for the request entity path, which must include "organizationsId" and "environmentsId". Returns: A dict of the API response in the form of: {"archiveDeployments": [list of archive deployments]} Raises: command_lib.apigee.errors.RequestError if there is an error with the API request. """ try: return request.ResponseToApiRequest( identifiers, entity_path=cls._entity_path[:-1], entity_collection=cls._entity_path[-1]) except errors.RequestError as error: raise error.RewrittenError("archive deployment", "list")
def Update(cls, identifiers, labels): """Calls the 'update' API for archive deployments. Args: identifiers: Dict of identifiers for the request entity path, which must include "organizationsId", "environmentsId" and "archiveDeploymentsId". labels: Dict of the labels proto to update, in the form of: {"labels": {"key1": "value1", "key2": "value2", ... "keyN": "valueN"}} Returns: A dict of the updated archive deployment. Raises: command_lib.apigee.errors.RequestError if there is an error with the API request. """ try: return request.ResponseToApiRequest(identifiers, entity_path=cls._entity_path, method="PATCH", body=json.dumps(labels)) except errors.RequestError as error: raise error.RewrittenError("archive deployment", "update")
def ProvisionOrganization(cls, project_id, org_info): return request.ResponseToApiRequest({"projectsId": project_id}, ["project"], method=":provisionOrganization", body=json.dumps(org_info))
def Environment(organization, prefix): """Creates a temporary Apigee environment. The environment will be automatically cleaned up upon exiting the context. Args: organization: the Apigee organization in which to create the environment. prefix: a string to include at the beginning of the environment name. Yields: the name of the created environment. Raises: RuntimeError: the environment could not be created in a reasonable amount of time. """ identifiers = {"organizationsId": organization} name = next(e2e_utils.GetResourceNameGenerator(prefix)) operation = request.ResponseToApiRequest(identifiers, ["organization"], "environment", method="POST", body=json.dumps({ "environment_id": name, "description": "created during an e2e test" })) try: # Environment creation is a long-running operation. Wait for it to complete. poll_attempts = 0 while poll_attempts < 30 and ("done" not in operation or not operation["done"]): time.sleep(1) operation = request.ResponseToApiRequest( { "operationsId": operation["name"].rsplit("/")[-1], "organizationsId": organization }, ["organization", "operation"]) poll_attempts += 1 if "done" not in operation or not operation["done"]: raise RuntimeError( "Took too long to create test environment. Status: " + json.dumps(operation)) yield name finally: identifiers["environmentsId"] = name # Just in case something didn't get properly cleaned up at a higher level, # undeploy everything in this environment. try: deployment_data = request.ResponseToApiRequest( identifiers, ["organization", "environment"], "deployment") except errors.EntityNotFoundError: deployment_data = {} if "deployments" in deployment_data: for hanging_deployment in deployment_data["deployments"]: try: deployment_id = identifiers.copy() deployment_id["apisId"] = hanging_deployment["apiProxy"] deployment_id["revisionsId"] = hanging_deployment[ "revision"] request.ResponseToApiRequest( deployment_id, ["organization", "environment", "api", "revision"], "deployment", method="DELETE") except (KeyError, errors.RequestError): # Clean up as much as possible, even if something went wrong with this # one. continue request.ResponseToApiRequest(identifiers, ["organization", "environment"], method="DELETE")