From 533e8739feb4864dd6f79f7cdf3ea0170a7e0027 Mon Sep 17 00:00:00 2001 From: Mason Chen Date: Mon, 4 Dec 2023 11:27:17 +0800 Subject: [PATCH] [Spring] API Portal try out & SCG response cache (#6988) --- src/spring/HISTORY.md | 4 + src/spring/azext_spring/_gateway_constant.py | 9 + src/spring/azext_spring/_help.py | 6 + src/spring/azext_spring/_params.py | 18 + .../azext_spring/_validators_enterprise.py | 70 + src/spring/azext_spring/api_portal.py | 18 +- src/spring/azext_spring/gateway.py | 94 +- .../recordings/test_response_cache.yaml | 7913 +++++++++++++++++ .../test_try_out_api_for_api_portal.yaml | 2202 +++++ .../latest/test_asa_api_portal_try_out_api.py | 37 + .../latest/test_asa_gateway_response_cache.py | 101 + .../latest/test_asa_gateway_validator.py | 191 + src/spring/setup.py | 2 +- 13 files changed, 10661 insertions(+), 4 deletions(-) create mode 100644 src/spring/azext_spring/_gateway_constant.py create mode 100644 src/spring/azext_spring/tests/latest/recordings/test_response_cache.yaml create mode 100644 src/spring/azext_spring/tests/latest/recordings/test_try_out_api_for_api_portal.yaml create mode 100644 src/spring/azext_spring/tests/latest/test_asa_api_portal_try_out_api.py create mode 100644 src/spring/azext_spring/tests/latest/test_asa_gateway_response_cache.py create mode 100644 src/spring/azext_spring/tests/latest/test_asa_gateway_validator.py diff --git a/src/spring/HISTORY.md b/src/spring/HISTORY.md index 1a281660259..7e02a4d07c6 100644 --- a/src/spring/HISTORY.md +++ b/src/spring/HISTORY.md @@ -1,5 +1,9 @@ Release History =============== +1.17.0 +--- +* Add arguments `--enable-api-try-out` in `spring api-portal update` + 1.16.0 --- * Add arguments `--enable-planned-maintenance`, `--planned-maintenance-day` and `--planned-maintenance-start-hour` in `az spring update` to support configuring Planned Maintenance. diff --git a/src/spring/azext_spring/_gateway_constant.py b/src/spring/azext_spring/_gateway_constant.py new file mode 100644 index 00000000000..8b8a190a02e --- /dev/null +++ b/src/spring/azext_spring/_gateway_constant.py @@ -0,0 +1,9 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE = "Route" +GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE = "Instance" +GATEWAY_RESPONSE_CACHE_SIZE_RESET_VALUE = "default" +GATEWAY_RESPONSE_CACHE_TTL_RESET_VALUE = "default" diff --git a/src/spring/azext_spring/_help.py b/src/spring/azext_spring/_help.py index 20c3d1b476c..de0f6ef6015 100644 --- a/src/spring/azext_spring/_help.py +++ b/src/spring/azext_spring/_help.py @@ -1095,6 +1095,12 @@ examples: - name: Update gateway property. text: az spring gateway update -s MyService -g MyResourceGroup --assign-endpoint true --https-only true + - name: Enable and configure response cache at Route level and set ttl to 5 minutes. + text: az spring gateway update -s MyService -g MyResourceGroup --enable-response-cache --response-cache-scope Route --response-cache-ttl 5m + - name: When response cache is enabled, update ttl to 3 minutes. + text: az spring gateway update -s MyService -g MyResourceGroup --response-cache-ttl 3m + - name: Disable response cache. + text: az spring gateway update -s MyService -g MyResourceGroup --enable-response-cache false """ helps['spring gateway restart'] = """ diff --git a/src/spring/azext_spring/_params.py b/src/spring/azext_spring/_params.py index bc3a24d1145..c7f1d0921e1 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -889,6 +889,7 @@ def prepare_logs_argument(c): c.argument('client_id', arg_group='Single Sign On (SSO)', help="The public identifier for the application.") c.argument('client_secret', arg_group='Single Sign On (SSO)', help="The secret known only to the application and the authorization server.") c.argument('issuer_uri', arg_group='Single Sign On (SSO)', help="The URI of Issuer Identifier.") + c.argument('enable_api_try_out', arg_type=get_three_state_flag(), arg_group='Try out API', help="Try out the API by sending requests and viewing responses in API portal.") with self.argument_context('spring gateway update') as c: c.argument('cpu', type=str, help='CPU resource quantity. Should be 500m or number of CPU cores.') @@ -926,6 +927,23 @@ def prepare_logs_argument(c): c.argument('addon_configs_file', arg_group='Add-on Configurations', help="The file path of JSON string of add-on configurations.") c.argument('apms', arg_group='APM', nargs='*', help="Space-separated list of APM reference names in Azure Spring Apps to integrate with Gateway.") + c.argument('enable_response_cache', + arg_type=get_three_state_flag(), + arg_group='Response Cache', + help='Enable response cache settings in Spring Cloud Gateway' + ) + c.argument('response_cache_scope', + arg_group='Response Cache', + help='Scope for response cache, available values are [Route, Instance]' + ) + c.argument('response_cache_size', + arg_group='Response Cache', + help='Maximum size of the cache that determines whether the cache needs to evict some entries. Examples are [1GB, 10MB, 100KB]. Use "default" to reset, and Gateway will manage this property.' + ) + c.argument('response_cache_ttl', + arg_group='Response Cache', + help='Time before a cached entry expires. Examples are [1h, 30m, 50s]. Use "default" to reset, and Gateway will manage this property.' + ) for scope in ['spring gateway custom-domain', 'spring api-portal custom-domain']: diff --git a/src/spring/azext_spring/_validators_enterprise.py b/src/spring/azext_spring/_validators_enterprise.py index e723d0ff900..4bdd7212dfe 100644 --- a/src/spring/azext_spring/_validators_enterprise.py +++ b/src/spring/azext_spring/_validators_enterprise.py @@ -18,6 +18,8 @@ from .vendored_sdks.appplatform.v2023_11_01_preview.models import (ApmReference, CertificateReference) from .vendored_sdks.appplatform.v2023_11_01_preview.models._app_platform_management_client_enums import (ApmType, ConfigurationServiceGeneration) +from ._gateway_constant import (GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE, GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE, + GATEWAY_RESPONSE_CACHE_SIZE_RESET_VALUE, GATEWAY_RESPONSE_CACHE_TTL_RESET_VALUE) from ._resource_quantity import validate_cpu as validate_and_normalize_cpu from ._resource_quantity import \ validate_memory as validate_and_normalize_memory @@ -351,6 +353,7 @@ def validate_acs_create(namespace): def validate_gateway_update(cmd, namespace): + _validate_gateway_response_cache(namespace) _validate_sso(namespace) validate_cpu(namespace) validate_memory(namespace) @@ -409,6 +412,73 @@ def _validate_gateway_secrets(namespace): namespace.secrets = secrets_dict +def _validate_gateway_response_cache(namespace): + _validate_gateway_response_cache_exclusive(namespace) + _validate_gateway_response_cache_scope(namespace) + _validate_gateway_response_cache_size(namespace) + _validate_gateway_response_cache_ttl(namespace) + + +def _validate_gateway_response_cache_exclusive(namespace): + if namespace.enable_response_cache is not None and namespace.enable_response_cache is False \ + and (namespace.response_cache_scope is not None + or namespace.response_cache_size is not None + or namespace.response_cache_ttl is not None): + raise InvalidArgumentValueError( + "Conflict detected: Parameters in ['--response-cache-scope', '--response-cache-scope', '--response-cache-ttl'] " + "cannot be set together with '--enable-response-cache false'.") + + +def _validate_gateway_response_cache_scope(namespace): + scope = namespace.response_cache_scope + if (scope is not None and not isinstance(scope, str)): + raise InvalidArgumentValueError("The allowed values for '--response-cache-scope' are [{}, {}]".format( + GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE, GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE + )) + if (scope is not None and isinstance(scope, str)): + scope = scope.lower() + if GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE.lower() != scope \ + and GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE.lower() != scope: + raise InvalidArgumentValueError("The allowed values for '--response-cache-scope' are [{}, {}]".format( + GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE, GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE + )) + # Normalize input + if GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE.lower() == scope: + namespace.response_cache_scope = GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE + else: + namespace.response_cache_scope = GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE + + +def _validate_gateway_response_cache_size(namespace): + if namespace.response_cache_size is not None: + size = namespace.response_cache_size + if type(size) != str: + raise InvalidArgumentValueError('--response-cache-size should be a string') + if GATEWAY_RESPONSE_CACHE_SIZE_RESET_VALUE.lower() == size.lower(): + # Normalize the input + namespace.response_cache_size = GATEWAY_RESPONSE_CACHE_SIZE_RESET_VALUE + else: + pattern = r"^[1-9][0-9]{0,9}(GB|MB|KB)$" + if not match(pattern, size): + raise InvalidArgumentValueError( + "Invalid response cache size '{}', the regex used to validate is '{}'".format(size, pattern)) + + +def _validate_gateway_response_cache_ttl(namespace): + if namespace.response_cache_ttl is not None: + ttl = namespace.response_cache_ttl + if type(ttl) != str: + raise InvalidArgumentValueError('--response-cache-ttl should be a string') + if GATEWAY_RESPONSE_CACHE_TTL_RESET_VALUE.lower() == ttl.lower(): + # Normalize the input + namespace.response_cache_ttl = GATEWAY_RESPONSE_CACHE_TTL_RESET_VALUE + else: + pattern = r"^[1-9][0-9]{0,9}(h|m|s)$" + if not match(pattern, ttl): + raise InvalidArgumentValueError( + "Invalid response cache ttl '{}', the regex used to validate is '{}'".format(ttl, pattern)) + + def validate_routes(namespace): if namespace.routes_json is not None and namespace.routes_file is not None: raise MutuallyExclusiveArgumentError("You can only specify either --routes-json or --routes-file.") diff --git a/src/spring/azext_spring/api_portal.py b/src/spring/azext_spring/api_portal.py index bd60bd1a123..8afea062d3b 100644 --- a/src/spring/azext_spring/api_portal.py +++ b/src/spring/azext_spring/api_portal.py @@ -51,7 +51,8 @@ def api_portal_update(cmd, client, resource_group, service, scope=None, client_id=None, client_secret=None, - issuer_uri=None): + issuer_uri=None, + enable_api_try_out=None): api_portal = client.api_portals.get(resource_group, service, DEFAULT_NAME) sso_properties = api_portal.properties.sso_properties @@ -67,11 +68,14 @@ def api_portal_update(cmd, client, resource_group, service, issuer_uri=issuer_uri, ) + target_api_try_out_state = _get_api_try_out_state(enable_api_try_out, api_portal.properties.api_try_out_enabled_state) + properties = models.ApiPortalProperties( public=assign_endpoint if assign_endpoint is not None else api_portal.properties.public, https_only=https_only if https_only is not None else api_portal.properties.https_only, gateway_ids=api_portal.properties.gateway_ids, - sso_properties=sso_properties + sso_properties=sso_properties, + api_try_out_enabled_state=target_api_try_out_state, ) sku = models.Sku(name=api_portal.sku.name, tier=api_portal.sku.tier, @@ -125,3 +129,13 @@ def api_portal_custom_domain_unbind(cmd, client, resource_group, service, domain client.api_portal_custom_domains.get(resource_group, service, DEFAULT_NAME, domain_name) return client.api_portal_custom_domains.begin_delete(resource_group, service, DEFAULT_NAME, domain_name) + + +def _get_api_try_out_state(enable_api_try_out, existing_api_try_out_enabled_state): + if enable_api_try_out is None: + return existing_api_try_out_enabled_state + + if enable_api_try_out: + return models.ApiPortalApiTryOutEnabledState.ENABLED + else: + return models.ApiPortalApiTryOutEnabledState.DISABLED diff --git a/src/spring/azext_spring/gateway.py b/src/spring/azext_spring/gateway.py index af5d936916f..354ac456676 100644 --- a/src/spring/azext_spring/gateway.py +++ b/src/spring/azext_spring/gateway.py @@ -12,6 +12,8 @@ from .custom import LOG_RUNNING_PROMPT from .vendored_sdks.appplatform.v2023_11_01_preview import models +from ._gateway_constant import (GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE, GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE, + GATEWAY_RESPONSE_CACHE_SIZE_RESET_VALUE, GATEWAY_RESPONSE_CACHE_TTL_RESET_VALUE) from ._utils import get_spring_sku logger = get_logger(__name__) @@ -60,6 +62,10 @@ def gateway_update(cmd, client, resource_group, service, addon_configs_json=None, addon_configs_file=None, apms=None, + enable_response_cache=None, + response_cache_scope=None, + response_cache_size=None, + response_cache_ttl=None, no_wait=False ): gateway = client.gateways.get(resource_group, service, DEFAULT_NAME) @@ -98,6 +104,15 @@ def gateway_update(cmd, client, resource_group, service, apms = _update_apms(client, resource_group, service, gateway.properties.apms, apms) + response_cache = _update_response_cache(client, + resource_group, + service, + gateway.properties.response_cache_properties, + enable_response_cache, + response_cache_scope, + response_cache_size, + response_cache_ttl) + model_properties = models.GatewayProperties( public=assign_endpoint if assign_endpoint is not None else gateway.properties.public, https_only=https_only if https_only is not None else gateway.properties.https_only, @@ -109,7 +124,8 @@ def gateway_update(cmd, client, resource_group, service, environment_variables=environment_variables, client_auth=client_auth, addon_configs=addon_configs, - resource_requests=resource_requests) + resource_requests=resource_requests, + response_cache_properties=response_cache) sku = models.Sku(name=gateway.sku.name, tier=gateway.sku.tier, capacity=instance_count or gateway.sku.capacity) @@ -364,3 +380,79 @@ def _route_config_property_convert(raw_json): replaced_key = re.sub('(?