PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/states/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/states/boto_apigateway.py |
""" Manage Apigateway Rest APIs =========================== .. versionadded:: 2016.11.0 :depends: - boto >= 2.8.0 - boto3 >= 1.2.1 - botocore >= 1.4.49 Create and destroy rest apis depending on a swagger version 2 definition file. Be aware that this interacts with Amazon's services, and so may incur charges. This module uses ``boto3``, which can be installed via package, or pip. This module accepts explicit vpc credentials but can also utilize IAM roles assigned to the instance through Instance Profiles. Dynamic credentials are then automatically obtained from AWS API and no further configuration is necessary. More information available `here <http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_. If IAM roles are not used you need to specify them either in a pillar file or in the minion's config file: .. code-block:: yaml vpc.keyid: GKTADJGHEIQSXMKKRBJ08H vpc.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile, either passed in as a dict, or as a string to pull from pillars or minion config: .. code-block:: yaml myprofile: keyid: GKTADJGHEIQSXMKKRBJ08H key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs region: us-east-1 .. code-block:: yaml Ensure Apigateway API exists: boto_apigateway.present: - name: myfunction - region: us-east-1 - keyid: GKTADJGHEIQSXMKKRBJ08H - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs """ import hashlib import logging import os import re import salt.utils.files import salt.utils.json import salt.utils.yaml log = logging.getLogger(__name__) def __virtual__(): """ Only load if boto is available. """ if "boto_apigateway.describe_apis" in __salt__: return "boto_apigateway" return (False, "boto_apigateway module could not be loaded") def present( name, api_name, swagger_file, stage_name, api_key_required, lambda_integration_role, lambda_region=None, stage_variables=None, region=None, key=None, keyid=None, profile=None, lambda_funcname_format="{stage}_{api}_{resource}_{method}", authorization_type="NONE", error_response_template=None, response_template=None, ): """ Ensure the spcified api_name with the corresponding swaggerfile is deployed to the given stage_name in AWS ApiGateway. this state currently only supports ApiGateway integration with AWS Lambda, and CORS support is handled through a Mock integration. There may be multiple deployments for the API object, each deployment is tagged with a description (i.e. unique label) in pretty printed json format consisting of the following key/values. .. code-block:: text { "api_name": api_name, "swagger_file": basename_of_swagger_file "swagger_file_md5sum": md5sum_of_swagger_file, "swagger_info_object": info_object_content_in_swagger_file } Please note that the name of the lambda function to be integrated will be derived via the provided lambda_funcname_format parameters: - the default lambda_funcname_format is a string with the following substitutable keys: "{stage}_{api}_{resource}_{method}". The user can choose to reorder the known keys. - the stage key corresponds to the stage_name passed in. - the api key corresponds to the api_name passed in. - the resource corresponds to the resource path defined in the passed swagger file. - the method corresponds to the method for a resource path defined in the passed swagger file. For the default lambda_funcname_format, given the following input: .. code-block:: python api_name = ' Test Service' stage_name = 'alpha' basePath = '/api' path = '/a/{b}/c' method = 'POST' We will end up with the following Lambda Function Name that will be looked up: 'test_service_alpha_a_b_c_post' The canconicalization of these input parameters is done in the following order: 1. lambda_funcname_format is formatted with the input parameters as passed, 2. resulting string is stripped for leading/trailing spaces, 3. path parameter's curly braces are removed from the resource path, 4. consecutive spaces and forward slashes in the paths are replaced with '_' 5. consecutive '_' are replaced with '_' Please note that for error response handling, the swagger file must have an error response model with the following schema. The lambda functions should throw exceptions for any non successful responses. An optional pattern field can be specified in errorMessage field to aid the response mapping from Lambda to the proper error return status codes. .. code-block:: yaml Error: type: object properties: stackTrace: type: array items: type: array items: type: string description: call stack errorType: type: string description: error type errorMessage: type: string description: | Error message, will be matched based on pattern. If no pattern is specified, the default pattern used for response mapping will be +*. name The name of the state definition api_name The name of the rest api that we want to ensure exists in AWS API Gateway swagger_file Name of the location of the swagger rest api definition file in YAML format. stage_name Name of the stage we want to be associated with the given api_name and swagger_file definition api_key_required True or False - whether the API Key is required to call API methods lambda_integration_role The name or ARN of the IAM role that the AWS ApiGateway assumes when it executes your lambda function to handle incoming requests lambda_region The region where we expect to find the lambda functions. This is used to determine the region where we should look for the Lambda Function for integration purposes. The region determination is based on the following priority: 1. lambda_region as passed in (is not None) 2. if lambda_region is None, use the region as if a boto_lambda function were executed without explicitly specifying lambda region. 3. if region determined in (2) is different than the region used by boto_apigateway functions, a final lookup will be attempted using the boto_apigateway region. stage_variables A dict with variables and their values, or a pillar key (string) that contains a dict with variables and their values. key and values in the dict must be strings. {'string': 'string'} region Region to connect to. key Secret key to be used. keyid Access key to be used. profile A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. lambda_funcname_format Please review the earlier example for the usage. The only substituable keys in the funcname format are {stage}, {api}, {resource}, {method}. Any other keys or positional substitution parameters will be flagged as an invalid input. authorization_type This field can be either 'NONE', or 'AWS_IAM'. This will be applied to all methods in the given swagger spec file. Default is set to 'NONE' error_response_template String value that defines the response template mapping that should be applied in cases error occurs. Refer to AWS documentation for details: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html If set to None, the following default value is used: .. code-block:: text '#set($inputRoot = $input.path(\'$\'))\\n' '{\\n' ' "errorMessage" : "$inputRoot.errorMessage",\\n' ' "errorType" : "$inputRoot.errorType",\\n' ' "stackTrace" : [\\n' '#foreach($stackTrace in $inputRoot.stackTrace)\\n' ' [\\n' '#foreach($elem in $stackTrace)\\n' ' "$elem"\\n' '#if($foreach.hasNext),#end\\n' '#end\\n' ' ]\\n' '#if($foreach.hasNext),#end\\n' '#end\\n' ' ]\\n' .. versionadded:: 2017.7.0 response_template String value that defines the response template mapping applied in case of success (including OPTIONS method) If set to None, empty ({}) template is assumed, which will transfer response from the lambda function as is. .. versionadded:: 2017.7.0 """ ret = {"name": name, "result": True, "comment": "", "changes": {}} try: common_args = dict( [("region", region), ("key", key), ("keyid", keyid), ("profile", profile)] ) # try to open the swagger file and basic validation swagger = _Swagger( api_name, stage_name, lambda_funcname_format, swagger_file, error_response_template, response_template, common_args, ) # retrieve stage variables stage_vars = _get_stage_variables(stage_variables) # verify if api and stage already exists ret = swagger.verify_api(ret) if ret.get("publish"): # there is a deployment label with signature matching the given api_name, # swagger file name, swagger file md5 sum, and swagger file info object # just reassociate the stage_name to the given deployment label. if __opts__["test"]: ret["comment"] = ( "[stage: {}] will be reassociated to an already available " "deployment that matched the given [api_name: {}] " "and [swagger_file: {}].\n" "Stage variables will be set " "to {}.".format(stage_name, api_name, swagger_file, stage_vars) ) ret["result"] = None return ret return swagger.publish_api(ret, stage_vars) if ret.get("current"): # already at desired state for the stage, swagger_file, and api_name if __opts__["test"]: ret["comment"] = ( "[stage: {}] is already at desired state with an associated " "deployment matching the given [api_name: {}] " "and [swagger_file: {}].\n" "Stage variables will be set " "to {}.".format(stage_name, api_name, swagger_file, stage_vars) ) ret["result"] = None return swagger.overwrite_stage_variables(ret, stage_vars) # there doesn't exist any previous deployments for the given swagger_file, we need # to redeploy the content of the swagger file to the api, models, and resources object # and finally create a new deployment and tie the stage_name to this new deployment if __opts__["test"]: ret["comment"] = ( "There is no deployment matching the given [api_name: {}] " "and [swagger_file: {}]. A new deployment will be " "created and the [stage_name: {}] will then be associated " "to the newly created deployment.\n" "Stage variables will be set " "to {}.".format(api_name, swagger_file, stage_name, stage_vars) ) ret["result"] = None return ret ret = swagger.deploy_api(ret) if ret.get("abort"): return ret ret = swagger.deploy_models(ret) if ret.get("abort"): return ret ret = swagger.deploy_resources( ret, api_key_required=api_key_required, lambda_integration_role=lambda_integration_role, lambda_region=lambda_region, authorization_type=authorization_type, ) if ret.get("abort"): return ret ret = swagger.publish_api(ret, stage_vars) except (ValueError, OSError) as e: ret["result"] = False ret["comment"] = f"{e.args}" return ret def _get_stage_variables(stage_variables): """ Helper function to retrieve stage variables from pillars/options, if the input is a string """ ret = dict() if stage_variables is None: return ret if isinstance(stage_variables, str): if stage_variables in __opts__: ret = __opts__[stage_variables] master_opts = __pillar__.get("master", {}) if stage_variables in master_opts: ret = master_opts[stage_variables] if stage_variables in __pillar__: ret = __pillar__[stage_variables] elif isinstance(stage_variables, dict): ret = stage_variables if not isinstance(ret, dict): ret = dict() return ret def absent( name, api_name, stage_name, nuke_api=False, region=None, key=None, keyid=None, profile=None, ): """ Ensure the stage_name associated with the given api_name deployed by boto_apigateway's present state is removed. If the currently associated deployment to the given stage_name has no other stages associated with it, the deployment will also be removed. name Name of the swagger file in YAML format api_name Name of the rest api on AWS ApiGateway to ensure is absent. stage_name Name of the stage to be removed irrespective of the swagger file content. If the current deployment associated with the stage_name has no other stages associated with it, the deployment will also be removed. nuke_api If True, removes the API itself only if there are no other stages associated with any other deployments once the given stage_name is removed. region Region to connect to. key Secret key to be used. keyid Access key to be used. profile A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. """ ret = {"name": name, "result": True, "comment": "", "changes": {}} try: common_args = dict( [("region", region), ("key", key), ("keyid", keyid), ("profile", profile)] ) swagger = _Swagger(api_name, stage_name, "", None, None, None, common_args) if not swagger.restApiId: ret["comment"] = f"[Rest API: {api_name}] does not exist." return ret if __opts__["test"]: if nuke_api: ret["comment"] = ( "[stage: {}] will be deleted, if there are no other " "active stages, the [api: {} will also be " "deleted.".format(stage_name, api_name) ) else: ret["comment"] = f"[stage: {stage_name}] will be deleted." ret["result"] = None return ret ret = swagger.delete_stage(ret) if ret.get("abort"): return ret if nuke_api and swagger.no_more_deployments_remain(): ret = swagger.delete_api(ret) except (ValueError, OSError) as e: ret["result"] = False ret["comment"] = f"{e.args}" return ret # Helper Swagger Class for swagger version 2.0 API specification def _gen_md5_filehash(fname, *args): """ helper function to generate a md5 hash of the swagger definition file any extra argument passed to the function is converted to a string and participates in the hash calculation """ _hash = hashlib.md5() with salt.utils.files.fopen(fname, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): _hash.update(chunk) for extra_arg in args: _hash.update(str(extra_arg).encode()) return _hash.hexdigest() def _dict_to_json_pretty(d, sort_keys=True): """ helper function to generate pretty printed json output """ return salt.utils.json.dumps( d, indent=4, separators=(",", ": "), sort_keys=sort_keys ) # Heuristic on whether or not the property name loosely matches given set of 'interesting' factors # If you are interested in IDs for example, 'id', 'blah_id', 'blahId' would all match def _name_matches(name, matches): """ Helper function to see if given name has any of the patterns in given matches """ for m in matches: if name.endswith(m): return True if name.lower().endswith("_" + m.lower()): return True if name.lower() == m.lower(): return True return False def _object_reducer( o, names=( "id", "name", "path", "httpMethod", "statusCode", "Created", "Deleted", "Updated", "Flushed", "Associated", "Disassociated", ), ): """ Helper function to reduce the amount of information that will be kept in the change log for API GW related return values """ result = {} if isinstance(o, dict): for k, v in o.items(): if isinstance(v, dict): reduced = v if k == "variables" else _object_reducer(v, names) if reduced or _name_matches(k, names): result[k] = reduced elif isinstance(v, list): newlist = [] for val in v: reduced = _object_reducer(val, names) if reduced or _name_matches(k, names): newlist.append(reduced) if newlist: result[k] = newlist else: if _name_matches(k, names): result[k] = v return result def _log_changes(ret, changekey, changevalue): """ For logging create/update/delete operations to AWS ApiGateway """ cl = ret["changes"].get("new", []) cl.append({changekey: _object_reducer(changevalue)}) ret["changes"]["new"] = cl return ret def _log_error_and_abort(ret, obj): """ helper function to update errors in the return structure """ ret["result"] = False ret["abort"] = True if "error" in obj: ret["comment"] = "{}".format(obj.get("error")) return ret class _Swagger: """ this is a helper class that holds the swagger definition file and the associated logic related to how to interpret the file and apply it to AWS Api Gateway. The main interface to the outside world is in deploy_api, deploy_models, and deploy_resources methods. """ SWAGGER_OBJ_V2_FIELDS = ( "swagger", "info", "host", "basePath", "schemes", "consumes", "produces", "paths", "definitions", "parameters", "responses", "securityDefinitions", "security", "tags", "externalDocs", ) # SWAGGER OBJECT V2 Fields that are required by boto apigateway states. SWAGGER_OBJ_V2_FIELDS_REQUIRED = ( "swagger", "info", "basePath", "schemes", "paths", "definitions", ) # SWAGGER OPERATION NAMES SWAGGER_OPERATION_NAMES = ( "get", "put", "post", "delete", "options", "head", "patch", ) SWAGGER_VERSIONS_SUPPORTED = ("2.0",) # VENDOR SPECIFIC FIELD PATTERNS VENDOR_EXT_PATTERN = re.compile("^x-") # JSON_SCHEMA_REF JSON_SCHEMA_DRAFT_4 = "http://json-schema.org/draft-04/schema#" # AWS integration templates for normal and options methods REQUEST_TEMPLATE = { "application/json": ( "#set($inputRoot = $input.path('$'))\n{\n\"header_params\" : {\n#set ($map" " = $input.params().header)\n#foreach( $param in $map.entrySet()" ' )\n"$param.key" : "$param.value" #if( $foreach.hasNext ),' ' #end\n#end\n},\n"query_params" : {\n#set ($map =' " $input.params().querystring)\n#foreach( $param in $map.entrySet()" ' )\n"$param.key" : "$param.value" #if( $foreach.hasNext ),' ' #end\n#end\n},\n"path_params" : {\n#set ($map =' " $input.params().path)\n#foreach( $param in $map.entrySet()" ' )\n"$param.key" : "$param.value" #if( $foreach.hasNext ),' ' #end\n#end\n},\n"apigw_context" : {\n"apiId":' ' "$context.apiId",\n"httpMethod": "$context.httpMethod",\n"requestId":' ' "$context.requestId",\n"resourceId":' ' "$context.resourceId",\n"resourcePath":' ' "$context.resourcePath",\n"stage": "$context.stage",\n"identity": {\n ' ' "user":"$context.identity.user",\n ' ' "userArn":"$context.identity.userArn",\n ' ' "userAgent":"$context.identity.userAgent",\n ' ' "sourceIp":"$context.identity.sourceIp",\n ' ' "cognitoIdentityId":"$context.identity.cognitoIdentityId",\n ' ' "cognitoIdentityPoolId":"$context.identity.cognitoIdentityPoolId",\n ' ' "cognitoAuthenticationType":"$context.identity.cognitoAuthenticationType",\n' ' "cognitoAuthenticationProvider":["$util.escapeJavaScript($context.identity.cognitoAuthenticationProvider)"],\n' ' "caller":"$context.identity.caller",\n ' ' "apiKey":"$context.identity.apiKey",\n ' ' "accountId":"$context.identity.accountId"\n}\n},\n"body_params" :' " $input.json('$'),\n\"stage_variables\": {\n#foreach($variable in" ' $stageVariables.keySet())\n"$variable":' ' "$util.escapeJavaScript($stageVariables.get($variable))"\n#if($foreach.hasNext),' " #end\n#end\n}\n}" ) } REQUEST_OPTION_TEMPLATE = {"application/json": '{"statusCode": 200}'} # AWS integration response template mapping to convert stackTrace part or the error # to a uniform format containing strings only. Swagger does not seem to allow defining # an array of non-uniform types, to it is not possible to create error model to match # exactly what comes out of lambda functions in case of error. RESPONSE_TEMPLATE = { "application/json": ( "#set($inputRoot = $input.path('$'))\n" "{\n" ' "errorMessage" : "$inputRoot.errorMessage",\n' ' "errorType" : "$inputRoot.errorType",\n' ' "stackTrace" : [\n' "#foreach($stackTrace in $inputRoot.stackTrace)\n" " [\n" "#foreach($elem in $stackTrace)\n" ' "$elem"\n' "#if($foreach.hasNext),#end\n" "#end\n" " ]\n" "#if($foreach.hasNext),#end\n" "#end\n" " ]\n" "}" ) } RESPONSE_OPTION_TEMPLATE = {} # This string should not be modified, every API created by this state will carry the description # below. AWS_API_DESCRIPTION = _dict_to_json_pretty( { "provisioned_by": "Salt boto_apigateway.present State", "context": "See deployment or stage description", } ) class SwaggerParameter: """ This is a helper class for the Swagger Parameter Object """ LOCATIONS = ("body", "query", "header", "path") def __init__(self, paramdict): self._paramdict = paramdict @property def location(self): """ returns location in the swagger parameter object """ _location = self._paramdict.get("in") if _location in _Swagger.SwaggerParameter.LOCATIONS: return _location raise ValueError( "Unsupported parameter location: {} in Parameter Object".format( _location ) ) @property def name(self): """ returns parameter name in the swagger parameter object """ _name = self._paramdict.get("name") if _name: if self.location == "header": return f"method.request.header.{_name}" elif self.location == "query": return f"method.request.querystring.{_name}" elif self.location == "path": return f"method.request.path.{_name}" return None raise ValueError( "Parameter must have a name: {}".format( _dict_to_json_pretty(self._paramdict) ) ) @property def schema(self): """ returns the name of the schema given the reference in the swagger parameter object """ if self.location == "body": _schema = self._paramdict.get("schema") if _schema: if "$ref" in _schema: schema_name = _schema.get("$ref").split("/")[-1] return schema_name raise ValueError( "Body parameter must have a JSON reference " "to the schema definition due to Amazon API restrictions: {}".format( self.name ) ) raise ValueError(f"Body parameter must have a schema: {self.name}") return None class SwaggerMethodResponse: """ Helper class for Swagger Method Response Object """ def __init__(self, r): self._r = r @property def schema(self): """ returns the name of the schema given the reference in the swagger method response object """ _schema = self._r.get("schema") if _schema: if "$ref" in _schema: return _schema.get("$ref").split("/")[-1] raise ValueError( "Method response must have a JSON reference " "to the schema definition: {}".format(_schema) ) return None @property def headers(self): """ returns the headers dictionary in the method response object """ _headers = self._r.get("headers", {}) return _headers def __init__( self, api_name, stage_name, lambda_funcname_format, swagger_file_path, error_response_template, response_template, common_aws_args, ): self._api_name = api_name self._stage_name = stage_name self._lambda_funcname_format = lambda_funcname_format self._common_aws_args = common_aws_args self._restApiId = "" self._deploymentId = "" self._error_response_template = error_response_template self._response_template = response_template if swagger_file_path is not None: if os.path.exists(swagger_file_path) and os.path.isfile(swagger_file_path): self._swagger_file = swagger_file_path self._md5_filehash = _gen_md5_filehash( self._swagger_file, error_response_template, response_template ) with salt.utils.files.fopen(self._swagger_file, "rb") as sf: self._cfg = salt.utils.yaml.safe_load(sf) self._swagger_version = "" else: raise OSError(f"Invalid swagger file path, {swagger_file_path}") self._validate_swagger_file() self._validate_lambda_funcname_format() self._resolve_api_id() def _is_http_error_rescode(self, code): """ Helper function to determine if the passed code is in the 400~599 range of http error codes """ return bool(re.match(r"^\s*[45]\d\d\s*$", code)) def _validate_error_response_model(self, paths, mods): """ Helper function to help validate the convention established in the swagger file on how to handle response code mapping/integration """ for path, ops in paths: for opname, opobj in ops.items(): if opname not in _Swagger.SWAGGER_OPERATION_NAMES: continue if "responses" not in opobj: raise ValueError( "missing mandatory responses field in path item object" ) for rescode, resobj in opobj.get("responses").items(): if not self._is_http_error_rescode(str(rescode)): continue # only check for response code from 400-599 if "schema" not in resobj: raise ValueError( "missing schema field in path {}, " "op {}, response {}".format(path, opname, rescode) ) schemaobj = resobj.get("schema") if "$ref" not in schemaobj: raise ValueError( "missing $ref field under schema in " "path {}, op {}, response {}".format(path, opname, rescode) ) schemaobjref = schemaobj.get("$ref", "/") modelname = schemaobjref.split("/")[-1] if modelname not in mods: raise ValueError( "model schema {} reference not found " "under /definitions".format(schemaobjref) ) model = mods.get(modelname) if model.get("type") != "object": raise ValueError( f"model schema {modelname} must be type object" ) if "properties" not in model: raise ValueError( "model schema {} must have properties fields".format( modelname ) ) modelprops = model.get("properties") if "errorMessage" not in modelprops: raise ValueError( "model schema {} must have errorMessage as a property to " "match AWS convention. If pattern is not set, .+ will " "be used".format(modelname) ) def _validate_lambda_funcname_format(self): """ Checks if the lambda function name format contains only known elements :return: True on success, ValueError raised on error """ try: if self._lambda_funcname_format: known_kwargs = dict(stage="", api="", resource="", method="") self._lambda_funcname_format.format(**known_kwargs) return True except Exception: # pylint: disable=broad-except raise ValueError( "Invalid lambda_funcname_format {}. Please review " "documentation for known substitutable keys".format( self._lambda_funcname_format ) ) def _validate_swagger_file(self): """ High level check/validation of the input swagger file based on https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md This is not a full schema compliance check, but rather make sure that the input file (YAML or JSON) can be read into a dictionary, and we check for the content of the Swagger Object for version and info. """ # check for any invalid fields for Swagger Object V2 for field in self._cfg: if ( field not in _Swagger.SWAGGER_OBJ_V2_FIELDS and not _Swagger.VENDOR_EXT_PATTERN.match(field) ): raise ValueError(f"Invalid Swagger Object Field: {field}") # check for Required Swagger fields by Saltstack boto apigateway state for field in _Swagger.SWAGGER_OBJ_V2_FIELDS_REQUIRED: if field not in self._cfg: raise ValueError(f"Missing Swagger Object Field: {field}") # check for Swagger Version self._swagger_version = self._cfg.get("swagger") if self._swagger_version not in _Swagger.SWAGGER_VERSIONS_SUPPORTED: raise ValueError( "Unsupported Swagger version: {},Supported versions are {}".format( self._swagger_version, _Swagger.SWAGGER_VERSIONS_SUPPORTED ) ) log.info(type(self._models)) self._validate_error_response_model(self.paths, self._models()) @property def md5_filehash(self): """ returns md5 hash for the swagger file """ return self._md5_filehash @property def info(self): """ returns the swagger info object as a dictionary """ info = self._cfg.get("info") if not info: raise ValueError("Info Object has no values") return info @property def info_json(self): """ returns the swagger info object as a pretty printed json string. """ return _dict_to_json_pretty(self.info) @property def rest_api_name(self): """ returns the name of the api """ return self._api_name @property def rest_api_version(self): """ returns the version field in the swagger info object """ version = self.info.get("version") if not version: raise ValueError("Missing version value in Info Object") return version def _models(self): """ returns an iterator for the models specified in the swagger file """ models = self._cfg.get("definitions") if not models: raise ValueError( "Definitions Object has no values, You need to define them in your" " swagger file" ) return models def models(self): """ generator to return the tuple of model and its schema to create on aws. """ model_dict = self._build_all_dependencies() while True: model = self._get_model_without_dependencies(model_dict) if not model: break yield (model, self._models().get(model)) @property def paths(self): """ returns an iterator for the relative resource paths specified in the swagger file """ paths = self._cfg.get("paths") if not paths: raise ValueError( "Paths Object has no values, You need to define them in your swagger" " file" ) for path in paths: if not path.startswith("/"): raise ValueError( f"Path object {path} should start with /. Please fix it" ) return paths.items() @property def basePath(self): """ returns the base path field as defined in the swagger file """ basePath = self._cfg.get("basePath", "") return basePath @property def restApiId(self): """ returns the rest api id as returned by AWS on creation of the rest api """ return self._restApiId @restApiId.setter def restApiId(self, restApiId): """ allows the assignment of the rest api id on creation of the rest api """ self._restApiId = restApiId @property def deployment_label_json(self): """ this property returns the unique description in pretty printed json for a particular api deployment """ return _dict_to_json_pretty(self.deployment_label) @property def deployment_label(self): """ this property returns the deployment label dictionary (mainly used by stage description) """ label = dict() label["swagger_info_object"] = self.info label["api_name"] = self.rest_api_name label["swagger_file"] = os.path.basename(self._swagger_file) label["swagger_file_md5sum"] = self.md5_filehash return label # methods to interact with boto_apigateway execution modules def _one_or_more_stages_remain(self, deploymentId): """ Helper function to find whether there are other stages still associated with a deployment """ stages = __salt__["boto_apigateway.describe_api_stages"]( restApiId=self.restApiId, deploymentId=deploymentId, **self._common_aws_args ).get("stages") return bool(stages) def no_more_deployments_remain(self): """ Helper function to find whether there are deployments left with stages associated """ no_more_deployments = True deployments = __salt__["boto_apigateway.describe_api_deployments"]( restApiId=self.restApiId, **self._common_aws_args ).get("deployments") if deployments: for deployment in deployments: deploymentId = deployment.get("id") stages = __salt__["boto_apigateway.describe_api_stages"]( restApiId=self.restApiId, deploymentId=deploymentId, **self._common_aws_args, ).get("stages") if stages: no_more_deployments = False break return no_more_deployments def _get_current_deployment_id(self): """ Helper method to find the deployment id that the stage name is currently assocaited with. """ deploymentId = "" stage = __salt__["boto_apigateway.describe_api_stage"]( restApiId=self.restApiId, stageName=self._stage_name, **self._common_aws_args, ).get("stage") if stage: deploymentId = stage.get("deploymentId") return deploymentId def _get_current_deployment_label(self): """ Helper method to find the deployment label that the stage_name is currently associated with. """ deploymentId = self._get_current_deployment_id() deployment = __salt__["boto_apigateway.describe_api_deployment"]( restApiId=self.restApiId, deploymentId=deploymentId, **self._common_aws_args ).get("deployment") if deployment: return deployment.get("description") return None def _get_desired_deployment_id(self): """ Helper method to return the deployment id matching the desired deployment label for this Swagger object based on the given api_name, swagger_file """ deployments = __salt__["boto_apigateway.describe_api_deployments"]( restApiId=self.restApiId, **self._common_aws_args ).get("deployments") if deployments: for deployment in deployments: if deployment.get("description") == self.deployment_label_json: return deployment.get("id") return "" def overwrite_stage_variables(self, ret, stage_variables): """ overwrite the given stage_name's stage variables with the given stage_variables """ res = __salt__["boto_apigateway.overwrite_api_stage_variables"]( restApiId=self.restApiId, stageName=self._stage_name, variables=stage_variables, **self._common_aws_args, ) if not res.get("overwrite"): ret["result"] = False ret["abort"] = True ret["comment"] = res.get("error") else: ret = _log_changes(ret, "overwrite_stage_variables", res.get("stage")) return ret def _set_current_deployment(self, stage_desc_json, stage_variables): """ Helper method to associate the stage_name to the given deploymentId and make this current """ stage = __salt__["boto_apigateway.describe_api_stage"]( restApiId=self.restApiId, stageName=self._stage_name, **self._common_aws_args, ).get("stage") if not stage: stage = __salt__["boto_apigateway.create_api_stage"]( restApiId=self.restApiId, stageName=self._stage_name, deploymentId=self._deploymentId, description=stage_desc_json, variables=stage_variables, **self._common_aws_args, ) if not stage.get("stage"): return {"set": False, "error": stage.get("error")} else: # overwrite the stage variables overwrite = __salt__["boto_apigateway.overwrite_api_stage_variables"]( restApiId=self.restApiId, stageName=self._stage_name, variables=stage_variables, **self._common_aws_args, ) if not overwrite.get("stage"): return {"set": False, "error": overwrite.get("error")} return __salt__["boto_apigateway.activate_api_deployment"]( restApiId=self.restApiId, stageName=self._stage_name, deploymentId=self._deploymentId, **self._common_aws_args, ) def _resolve_api_id(self): """ returns an Api Id that matches the given api_name and the hardcoded _Swagger.AWS_API_DESCRIPTION as the api description """ apis = __salt__["boto_apigateway.describe_apis"]( name=self.rest_api_name, description=_Swagger.AWS_API_DESCRIPTION, **self._common_aws_args, ).get("restapi") if apis: if len(apis) == 1: self.restApiId = apis[0].get("id") else: raise ValueError( "Multiple APIs matching given name {} and description {}".format( self.rest_api_name, self.info_json ) ) def delete_stage(self, ret): """ Method to delete the given stage_name. If the current deployment tied to the given stage_name has no other stages associated with it, the deployment will be removed as well """ deploymentId = self._get_current_deployment_id() if deploymentId: result = __salt__["boto_apigateway.delete_api_stage"]( restApiId=self.restApiId, stageName=self._stage_name, **self._common_aws_args, ) if not result.get("deleted"): ret["abort"] = True ret["result"] = False ret["comment"] = "delete_stage delete_api_stage, {}".format( result.get("error") ) else: # check if it is safe to delete the deployment as well. if not self._one_or_more_stages_remain(deploymentId): result = __salt__["boto_apigateway.delete_api_deployment"]( restApiId=self.restApiId, deploymentId=deploymentId, **self._common_aws_args, ) if not result.get("deleted"): ret["abort"] = True ret["result"] = False ret["comment"] = ( "delete_stage delete_api_deployment, {}".format( result.get("error") ) ) else: ret["comment"] = "stage {} has been deleted.\n".format( self._stage_name ) else: # no matching stage_name/deployment found ret["comment"] = f"stage {self._stage_name} does not exist" return ret def verify_api(self, ret): """ this method helps determine if the given stage_name is already on a deployment label matching the input api_name, swagger_file. If yes, returns abort with comment indicating already at desired state. If not and there is previous deployment labels in AWS matching the given input api_name and swagger file, indicate to the caller that we only need to reassociate stage_name to the previously existing deployment label. """ if self.restApiId: deployed_label_json = self._get_current_deployment_label() if deployed_label_json == self.deployment_label_json: ret["comment"] = ( "Already at desired state, the stage {} is already at the desired " "deployment label:\n{}".format( self._stage_name, deployed_label_json ) ) ret["current"] = True return ret else: self._deploymentId = self._get_desired_deployment_id() if self._deploymentId: ret["publish"] = True return ret def publish_api(self, ret, stage_variables): """ this method tie the given stage_name to a deployment matching the given swagger_file """ stage_desc = dict() stage_desc["current_deployment_label"] = self.deployment_label stage_desc_json = _dict_to_json_pretty(stage_desc) if self._deploymentId: # just do a reassociate of stage_name to an already existing deployment res = self._set_current_deployment(stage_desc_json, stage_variables) if not res.get("set"): ret["abort"] = True ret["result"] = False ret["comment"] = res.get("error") else: ret = _log_changes( ret, "publish_api (reassociate deployment, set stage_variables)", res.get("response"), ) else: # no deployment existed for the given swagger_file for this Swagger object res = __salt__["boto_apigateway.create_api_deployment"]( restApiId=self.restApiId, stageName=self._stage_name, stageDescription=stage_desc_json, description=self.deployment_label_json, variables=stage_variables, **self._common_aws_args, ) if not res.get("created"): ret["abort"] = True ret["result"] = False ret["comment"] = res.get("error") else: ret = _log_changes( ret, "publish_api (new deployment)", res.get("deployment") ) return ret def _cleanup_api(self): """ Helper method to clean up resources and models if we detected a change in the swagger file for a stage """ resources = __salt__["boto_apigateway.describe_api_resources"]( restApiId=self.restApiId, **self._common_aws_args ) if resources.get("resources"): res = resources.get("resources")[1:] res.reverse() for resource in res: delres = __salt__["boto_apigateway.delete_api_resources"]( restApiId=self.restApiId, path=resource.get("path"), **self._common_aws_args, ) if not delres.get("deleted"): return delres models = __salt__["boto_apigateway.describe_api_models"]( restApiId=self.restApiId, **self._common_aws_args ) if models.get("models"): for model in models.get("models"): delres = __salt__["boto_apigateway.delete_api_model"]( restApiId=self.restApiId, modelName=model.get("name"), **self._common_aws_args, ) if not delres.get("deleted"): return delres return {"deleted": True} def deploy_api(self, ret): """ this method create the top level rest api in AWS apigateway """ if self.restApiId: res = self._cleanup_api() if not res.get("deleted"): ret["comment"] = f"Failed to cleanup restAreId {self.restApiId}" ret["abort"] = True ret["result"] = False return ret return ret response = __salt__["boto_apigateway.create_api"]( name=self.rest_api_name, description=_Swagger.AWS_API_DESCRIPTION, **self._common_aws_args, ) if not response.get("created"): ret["result"] = False ret["abort"] = True if "error" in response: ret["comment"] = "Failed to create rest api: {}.".format( response["error"]["message"] ) return ret self.restApiId = response.get("restapi", {}).get("id") return _log_changes(ret, "deploy_api", response.get("restapi")) def delete_api(self, ret): """ Method to delete a Rest Api named defined in the swagger file's Info Object's title value. ret a dictionary for returning status to Saltstack """ exists_response = __salt__["boto_apigateway.api_exists"]( name=self.rest_api_name, description=_Swagger.AWS_API_DESCRIPTION, **self._common_aws_args, ) if exists_response.get("exists"): if __opts__["test"]: ret["comment"] = "Rest API named {} is set to be deleted.".format( self.rest_api_name ) ret["result"] = None ret["abort"] = True return ret delete_api_response = __salt__["boto_apigateway.delete_api"]( name=self.rest_api_name, description=_Swagger.AWS_API_DESCRIPTION, **self._common_aws_args, ) if not delete_api_response.get("deleted"): ret["result"] = False ret["abort"] = True if "error" in delete_api_response: ret["comment"] = "Failed to delete rest api: {}.".format( delete_api_response["error"]["message"] ) return ret ret = _log_changes(ret, "delete_api", delete_api_response) else: ret["comment"] = "api already absent for swagger file: {}, desc: {}".format( self.rest_api_name, self.info_json ) return ret def _aws_model_ref_from_swagger_ref(self, r): """ Helper function to reference models created on aws apigw """ model_name = r.split("/")[-1] return "https://apigateway.amazonaws.com/restapis/{}/models/{}".format( self.restApiId, model_name ) def _update_schema_to_aws_notation(self, schema): """ Helper function to map model schema to aws notation """ result = {} for k, v in schema.items(): if k == "$ref": v = self._aws_model_ref_from_swagger_ref(v) if isinstance(v, dict): v = self._update_schema_to_aws_notation(v) result[k] = v return result def _build_dependent_model_list(self, obj_schema): """ Helper function to build the list of models the given object schema is referencing. """ dep_models_list = [] if obj_schema: obj_schema["type"] = obj_schema.get("type", "object") if obj_schema["type"] == "array": dep_models_list.extend( self._build_dependent_model_list(obj_schema.get("items", {})) ) else: ref = obj_schema.get("$ref") if ref: ref_obj_model = ref.split("/")[-1] ref_obj_schema = self._models().get(ref_obj_model) dep_models_list.extend(self._build_dependent_model_list(ref_obj_schema)) dep_models_list.extend([ref_obj_model]) else: # need to walk each property object properties = obj_schema.get("properties") if properties: for _, prop_obj_schema in properties.items(): dep_models_list.extend( self._build_dependent_model_list(prop_obj_schema) ) return list(set(dep_models_list)) def _build_all_dependencies(self): """ Helper function to build a map of model to their list of model reference dependencies """ ret = {} for model, schema in self._models().items(): dep_list = self._build_dependent_model_list(schema) ret[model] = dep_list return ret def _get_model_without_dependencies(self, models_dict): """ Helper function to find the next model that should be created """ next_model = None if not models_dict: return next_model for model, dependencies in models_dict.items(): if dependencies == []: next_model = model break if next_model is None: raise ValueError( "incomplete model definitions, models in dependency " "list not defined: {}".format(models_dict) ) # remove the model from other depednencies before returning models_dict.pop(next_model) for model, dep_list in models_dict.items(): if next_model in dep_list: dep_list.remove(next_model) return next_model def deploy_models(self, ret): """ Method to deploy swagger file's definition objects and associated schema to AWS Apigateway as Models ret a dictionary for returning status to Saltstack """ for model, schema in self.models(): # add in a few attributes into the model schema that AWS expects # _schema = schema.copy() _schema = self._update_schema_to_aws_notation(schema) _schema.update( { "$schema": _Swagger.JSON_SCHEMA_DRAFT_4, "title": f"{model} Schema", } ) # check to see if model already exists, aws has 2 default models [Empty, Error] # which may need upate with data from swagger file model_exists_response = __salt__["boto_apigateway.api_model_exists"]( restApiId=self.restApiId, modelName=model, **self._common_aws_args ) if model_exists_response.get("exists"): update_model_schema_response = __salt__[ "boto_apigateway.update_api_model_schema" ]( restApiId=self.restApiId, modelName=model, schema=_dict_to_json_pretty(_schema), **self._common_aws_args, ) if not update_model_schema_response.get("updated"): ret["result"] = False ret["abort"] = True if "error" in update_model_schema_response: ret["comment"] = ( "Failed to update existing model {} with schema {}, " "error: {}".format( model, _dict_to_json_pretty(schema), update_model_schema_response["error"]["message"], ) ) return ret ret = _log_changes(ret, "deploy_models", update_model_schema_response) else: create_model_response = __salt__["boto_apigateway.create_api_model"]( restApiId=self.restApiId, modelName=model, modelDescription=model, schema=_dict_to_json_pretty(_schema), contentType="application/json", **self._common_aws_args, ) if not create_model_response.get("created"): ret["result"] = False ret["abort"] = True if "error" in create_model_response: ret["comment"] = ( "Failed to create model {}, schema {}, error: {}".format( model, _dict_to_json_pretty(schema), create_model_response["error"]["message"], ) ) return ret ret = _log_changes(ret, "deploy_models", create_model_response) return ret def _lambda_name(self, resourcePath, httpMethod): """ Helper method to construct lambda name based on the rule specified in doc string of boto_apigateway.api_present function """ lambda_name = self._lambda_funcname_format.format( stage=self._stage_name, api=self.rest_api_name, resource=resourcePath, method=httpMethod, ) lambda_name = lambda_name.strip() lambda_name = re.sub(r"{|}", "", lambda_name) lambda_name = re.sub(r"\s+|/", "_", lambda_name).lower() return re.sub(r"_+", "_", lambda_name) def _lambda_uri(self, lambda_name, lambda_region): """ Helper Method to construct the lambda uri for use in method integration """ profile = self._common_aws_args.get("profile") region = self._common_aws_args.get("region") lambda_region = __utils__["boto3.get_region"]("lambda", lambda_region, profile) apigw_region = __utils__["boto3.get_region"]("apigateway", region, profile) lambda_desc = __salt__["boto_lambda.describe_function"]( lambda_name, **self._common_aws_args ) if lambda_region != apigw_region: if not lambda_desc.get("function"): # try look up in the same region as the apigateway as well if previous lookup failed lambda_desc = __salt__["boto_lambda.describe_function"]( lambda_name, **self._common_aws_args ) if not lambda_desc.get("function"): raise ValueError( "Could not find lambda function {} in regions [{}, {}].".format( lambda_name, lambda_region, apigw_region ) ) lambda_arn = lambda_desc.get("function").get("FunctionArn") lambda_uri = ( "arn:aws:apigateway:{}:lambda:path/2015-03-31" "/functions/{}/invocations".format(apigw_region, lambda_arn) ) return lambda_uri def _parse_method_data(self, method_name, method_data): """ Helper function to construct the method request params, models, request_templates and integration_type values needed to configure method request integration/mappings. """ method_params = {} method_models = {} if "parameters" in method_data: for param in method_data["parameters"]: p = _Swagger.SwaggerParameter(param) if p.name: method_params[p.name] = True if p.schema: method_models["application/json"] = p.schema request_templates = ( _Swagger.REQUEST_OPTION_TEMPLATE if method_name == "options" else _Swagger.REQUEST_TEMPLATE ) integration_type = "MOCK" if method_name == "options" else "AWS" return { "params": method_params, "models": method_models, "request_templates": request_templates, "integration_type": integration_type, } def _find_patterns(self, o): result = [] if isinstance(o, dict): for k, v in o.items(): if isinstance(v, dict): result.extend(self._find_patterns(v)) else: if k == "pattern": result.append(v) return result def _get_pattern_for_schema(self, schema_name, httpStatus): """ returns the pattern specified in a response schema """ defaultPattern = ".+" if self._is_http_error_rescode(httpStatus) else ".*" model = self._models().get(schema_name) patterns = self._find_patterns(model) return patterns[0] if patterns else defaultPattern def _get_response_template(self, method_name, http_status): if method_name == "options" or not self._is_http_error_rescode(http_status): response_templates = ( {"application/json": self._response_template} if self._response_template else self.RESPONSE_OPTION_TEMPLATE ) else: response_templates = ( {"application/json": self._error_response_template} if self._error_response_template else self.RESPONSE_TEMPLATE ) return response_templates def _parse_method_response(self, method_name, method_response, httpStatus): """ Helper function to construct the method response params, models, and integration_params values needed to configure method response integration/mappings. """ method_response_models = {} method_response_pattern = ".*" if method_response.schema: method_response_models["application/json"] = method_response.schema method_response_pattern = self._get_pattern_for_schema( method_response.schema, httpStatus ) method_response_params = {} method_integration_response_params = {} for header in method_response.headers: response_header = f"method.response.header.{header}" method_response_params[response_header] = False header_data = method_response.headers.get(header) method_integration_response_params[response_header] = ( "'{}'".format(header_data.get("default")) if "default" in header_data else "'*'" ) response_templates = self._get_response_template(method_name, httpStatus) return { "params": method_response_params, "models": method_response_models, "integration_params": method_integration_response_params, "pattern": method_response_pattern, "response_templates": response_templates, } def _deploy_method( self, ret, resource_path, method_name, method_data, api_key_required, lambda_integration_role, lambda_region, authorization_type, ): """ Method to create a method for the given resource path, along with its associated request and response integrations. ret a dictionary for returning status to Saltstack resource_path the full resource path where the named method_name will be associated with. method_name a string that is one of the following values: 'delete', 'get', 'head', 'options', 'patch', 'post', 'put' method_data the value dictionary for this method in the swagger definition file. api_key_required True or False, whether api key is required to access this method. lambda_integration_role name of the IAM role or IAM role arn that Api Gateway will assume when executing the associated lambda function lambda_region the region for the lambda function that Api Gateway will integrate to. authorization_type 'NONE' or 'AWS_IAM' """ method = self._parse_method_data(method_name.lower(), method_data) # for options method to enable CORS, api_key_required will be set to False always. # authorization_type will be set to 'NONE' always. if method_name.lower() == "options": api_key_required = False authorization_type = "NONE" m = __salt__["boto_apigateway.create_api_method"]( restApiId=self.restApiId, resourcePath=resource_path, httpMethod=method_name.upper(), authorizationType=authorization_type, apiKeyRequired=api_key_required, requestParameters=method.get("params"), requestModels=method.get("models"), **self._common_aws_args, ) if not m.get("created"): ret = _log_error_and_abort(ret, m) return ret ret = _log_changes(ret, "_deploy_method.create_api_method", m) lambda_uri = "" if method_name.lower() != "options": lambda_uri = self._lambda_uri( self._lambda_name(resource_path, method_name), lambda_region=lambda_region, ) # NOTE: integration method is set to POST always, as otherwise AWS makes wrong assumptions # about the intent of the call. HTTP method will be passed to lambda as part of the API gateway context integration = __salt__["boto_apigateway.create_api_integration"]( restApiId=self.restApiId, resourcePath=resource_path, httpMethod=method_name.upper(), integrationType=method.get("integration_type"), integrationHttpMethod="POST", uri=lambda_uri, credentials=lambda_integration_role, requestTemplates=method.get("request_templates"), **self._common_aws_args, ) if not integration.get("created"): ret = _log_error_and_abort(ret, integration) return ret ret = _log_changes(ret, "_deploy_method.create_api_integration", integration) if "responses" in method_data: for response, response_data in method_data["responses"].items(): httpStatus = str(response) method_response = self._parse_method_response( method_name.lower(), _Swagger.SwaggerMethodResponse(response_data), httpStatus, ) mr = __salt__["boto_apigateway.create_api_method_response"]( restApiId=self.restApiId, resourcePath=resource_path, httpMethod=method_name.upper(), statusCode=httpStatus, responseParameters=method_response.get("params"), responseModels=method_response.get("models"), **self._common_aws_args, ) if not mr.get("created"): ret = _log_error_and_abort(ret, mr) return ret ret = _log_changes(ret, "_deploy_method.create_api_method_response", mr) mir = __salt__["boto_apigateway.create_api_integration_response"]( restApiId=self.restApiId, resourcePath=resource_path, httpMethod=method_name.upper(), statusCode=httpStatus, selectionPattern=method_response.get("pattern"), responseParameters=method_response.get("integration_params"), responseTemplates=method_response.get("response_templates"), **self._common_aws_args, ) if not mir.get("created"): ret = _log_error_and_abort(ret, mir) return ret ret = _log_changes( ret, "_deploy_method.create_api_integration_response", mir ) else: raise ValueError( f"No responses specified for {resource_path} {method_name}" ) return ret def deploy_resources( self, ret, api_key_required, lambda_integration_role, lambda_region, authorization_type, ): """ Method to deploy resources defined in the swagger file. ret a dictionary for returning status to Saltstack api_key_required True or False, whether api key is required to access this method. lambda_integration_role name of the IAM role or IAM role arn that Api Gateway will assume when executing the associated lambda function lambda_region the region for the lambda function that Api Gateway will integrate to. authorization_type 'NONE' or 'AWS_IAM' """ for path, pathData in self.paths: resource = __salt__["boto_apigateway.create_api_resources"]( restApiId=self.restApiId, path=path, **self._common_aws_args ) if not resource.get("created"): ret = _log_error_and_abort(ret, resource) return ret ret = _log_changes(ret, "deploy_resources", resource) for method, method_data in pathData.items(): if method in _Swagger.SWAGGER_OPERATION_NAMES: ret = self._deploy_method( ret, path, method, method_data, api_key_required, lambda_integration_role, lambda_region, authorization_type, ) return ret def usage_plan_present( name, plan_name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None, ): """ Ensure the spcifieda usage plan with the corresponding metrics is deployed .. versionadded:: 2017.7.0 name name of the state plan_name [Required] name of the usage plan throttle [Optional] throttling parameters expressed as a dictionary. If provided, at least one of the throttling parameters must be present rateLimit rate per second at which capacity bucket is populated burstLimit maximum rate allowed quota [Optional] quota on the number of api calls permitted by the plan. If provided, limit and period must be present limit [Required] number of calls permitted per quota period offset [Optional] number of calls to be subtracted from the limit at the beginning of the period period [Required] period to which quota applies. Must be DAY, WEEK or MONTH .. code-block:: yaml UsagePlanPresent: boto_apigateway.usage_plan_present: - plan_name: my_usage_plan - throttle: rateLimit: 70 burstLimit: 100 - quota: limit: 1000 offset: 0 period: DAY - profile: my_profile """ func_params = locals() ret = {"name": name, "result": True, "comment": "", "changes": {}} try: common_args = dict( [("region", region), ("key", key), ("keyid", keyid), ("profile", profile)] ) existing = __salt__["boto_apigateway.describe_usage_plans"]( name=plan_name, **common_args ) if "error" in existing: ret["result"] = False ret["comment"] = "Failed to describe existing usage plans" return ret if not existing["plans"]: # plan does not exist, we need to create it if __opts__["test"]: ret["comment"] = "a new usage plan {} would be created".format( plan_name ) ret["result"] = None return ret result = __salt__["boto_apigateway.create_usage_plan"]( name=plan_name, description=description, throttle=throttle, quota=quota, **common_args, ) if "error" in result: ret["result"] = False ret["comment"] = "Failed to create a usage plan {}, {}".format( plan_name, result["error"] ) return ret ret["changes"]["old"] = {"plan": None} ret["comment"] = f"A new usage plan {plan_name} has been created" else: # need an existing plan modified to match given value plan = existing["plans"][0] needs_updating = False modifiable_params = ( ("throttle", ("rateLimit", "burstLimit")), ("quota", ("limit", "offset", "period")), ) for p, fields in modifiable_params: for f in fields: actual_param = ( {} if func_params.get(p) is None else func_params.get(p) ) if plan.get(p, {}).get(f, None) != actual_param.get(f, None): needs_updating = True break if not needs_updating: ret["comment"] = "usage plan {} is already in a correct state".format( plan_name ) ret["result"] = True return ret if __opts__["test"]: ret["comment"] = "a new usage plan {} would be updated".format( plan_name ) ret["result"] = None return ret result = __salt__["boto_apigateway.update_usage_plan"]( plan["id"], throttle=throttle, quota=quota, **common_args ) if "error" in result: ret["result"] = False ret["comment"] = "Failed to update a usage plan {}, {}".format( plan_name, result["error"] ) return ret ret["changes"]["old"] = {"plan": plan} ret["comment"] = f"usage plan {plan_name} has been updated" newstate = __salt__["boto_apigateway.describe_usage_plans"]( name=plan_name, **common_args ) if "error" in existing: ret["result"] = False ret["comment"] = "Failed to describe existing usage plans after updates" return ret ret["changes"]["new"] = {"plan": newstate["plans"][0]} except (ValueError, OSError) as e: ret["result"] = False ret["comment"] = f"{e.args}" return ret def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profile=None): """ Ensures usage plan identified by name is no longer present .. versionadded:: 2017.7.0 name name of the state plan_name name of the plan to remove .. code-block:: yaml usage plan absent: boto_apigateway.usage_plan_absent: - plan_name: my_usage_plan - profile: my_profile """ ret = {"name": name, "result": True, "comment": "", "changes": {}} try: common_args = dict( [("region", region), ("key", key), ("keyid", keyid), ("profile", profile)] ) existing = __salt__["boto_apigateway.describe_usage_plans"]( name=plan_name, **common_args ) if "error" in existing: ret["result"] = False ret["comment"] = "Failed to describe existing usage plans" return ret if not existing["plans"]: ret["comment"] = f"Usage plan {plan_name} does not exist already" return ret if __opts__["test"]: ret["comment"] = "Usage plan {} exists and would be deleted".format( plan_name ) ret["result"] = None return ret plan_id = existing["plans"][0]["id"] result = __salt__["boto_apigateway.delete_usage_plan"](plan_id, **common_args) if "error" in result: ret["result"] = False ret["comment"] = "Failed to delete usage plan {}, {}".format( plan_name, result ) return ret ret["comment"] = f"Usage plan {plan_name} has been deleted" ret["changes"]["old"] = {"plan": existing["plans"][0]} ret["changes"]["new"] = {"plan": None} except (ValueError, OSError) as e: ret["result"] = False ret["comment"] = f"{e.args}" return ret def usage_plan_association_present( name, plan_name, api_stages, region=None, key=None, keyid=None, profile=None ): """ Ensures usage plan identified by name is added to provided api_stages .. versionadded:: 2017.7.0 name name of the state plan_name name of the plan to use api_stages list of dictionaries, where each dictionary consists of the following keys: apiId apiId of the api to attach usage plan to stage stage name of the api to attach usage plan to .. code-block:: yaml UsagePlanAssociationPresent: boto_apigateway.usage_plan_association_present: - plan_name: my_plan - api_stages: - apiId: 9kb0404ec0 stage: my_stage - apiId: l9v7o2aj90 stage: my_stage - profile: my_profile """ ret = {"name": name, "result": True, "comment": "", "changes": {}} try: common_args = dict( [("region", region), ("key", key), ("keyid", keyid), ("profile", profile)] ) existing = __salt__["boto_apigateway.describe_usage_plans"]( name=plan_name, **common_args ) if "error" in existing: ret["result"] = False ret["comment"] = "Failed to describe existing usage plans" return ret if not existing["plans"]: ret["comment"] = f"Usage plan {plan_name} does not exist" ret["result"] = False return ret if len(existing["plans"]) != 1: ret["comment"] = ( "There are multiple usage plans with the same name - it is not" " supported" ) ret["result"] = False return ret plan = existing["plans"][0] plan_id = plan["id"] plan_stages = plan.get("apiStages", []) stages_to_add = [] for api in api_stages: if api not in plan_stages: stages_to_add.append(api) if not stages_to_add: ret["comment"] = "Usage plan is already asssociated to all api stages" return ret result = __salt__["boto_apigateway.attach_usage_plan_to_apis"]( plan_id, stages_to_add, **common_args ) if "error" in result: ret["comment"] = ( "Failed to associate a usage plan {} to the apis {}, {}".format( plan_name, stages_to_add, result["error"] ) ) ret["result"] = False return ret ret["comment"] = "successfully associated usage plan to apis" ret["changes"]["old"] = plan_stages ret["changes"]["new"] = result.get("result", {}).get("apiStages", []) except (ValueError, OSError) as e: ret["result"] = False ret["comment"] = f"{e.args}" return ret def usage_plan_association_absent( name, plan_name, api_stages, region=None, key=None, keyid=None, profile=None ): """ Ensures usage plan identified by name is removed from provided api_stages If a plan is associated to stages not listed in api_stages parameter, those associations remain intact. .. versionadded:: 2017.7.0 name name of the state plan_name name of the plan to use api_stages list of dictionaries, where each dictionary consists of the following keys: apiId apiId of the api to detach usage plan from stage stage name of the api to detach usage plan from .. code-block:: yaml UsagePlanAssociationAbsent: boto_apigateway.usage_plan_association_absent: - plan_name: my_plan - api_stages: - apiId: 9kb0404ec0 stage: my_stage - apiId: l9v7o2aj90 stage: my_stage - profile: my_profile """ ret = {"name": name, "result": True, "comment": "", "changes": {}} try: common_args = dict( [("region", region), ("key", key), ("keyid", keyid), ("profile", profile)] ) existing = __salt__["boto_apigateway.describe_usage_plans"]( name=plan_name, **common_args ) if "error" in existing: ret["result"] = False ret["comment"] = "Failed to describe existing usage plans" return ret if not existing["plans"]: ret["comment"] = f"Usage plan {plan_name} does not exist" ret["result"] = False return ret if len(existing["plans"]) != 1: ret["comment"] = ( "There are multiple usage plans with the same name - it is not" " supported" ) ret["result"] = False return ret plan = existing["plans"][0] plan_id = plan["id"] plan_stages = plan.get("apiStages", []) if not plan_stages: ret["comment"] = "Usage plan {} has no associated stages already".format( plan_name ) return ret stages_to_remove = [] for api in api_stages: if api in plan_stages: stages_to_remove.append(api) if not stages_to_remove: ret["comment"] = "Usage plan is already not asssociated to any api stages" return ret result = __salt__["boto_apigateway.detach_usage_plan_from_apis"]( plan_id, stages_to_remove, **common_args ) if "error" in result: ret["comment"] = ( "Failed to disassociate a usage plan {} from the apis {}, {}".format( plan_name, stages_to_remove, result["error"] ) ) ret["result"] = False return ret ret["comment"] = "successfully disassociated usage plan from apis" ret["changes"]["old"] = plan_stages ret["changes"]["new"] = result.get("result", {}).get("apiStages", []) except (ValueError, OSError) as e: ret["result"] = False ret["comment"] = f"{e.args}" return ret