Skip to content

Commit

Permalink
[Spring] API Portal try out & SCG response cache (Azure#6988)
Browse files Browse the repository at this point in the history
  • Loading branch information
jiec-msft authored Dec 4, 2023
1 parent 06bc948 commit 533e873
Show file tree
Hide file tree
Showing 13 changed files with 10,661 additions and 4 deletions.
4 changes: 4 additions & 0 deletions src/spring/HISTORY.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
9 changes: 9 additions & 0 deletions src/spring/azext_spring/_gateway_constant.py
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 6 additions & 0 deletions src/spring/azext_spring/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'] = """
Expand Down
18 changes: 18 additions & 0 deletions src/spring/azext_spring/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down Expand Up @@ -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']:
Expand Down
70 changes: 70 additions & 0 deletions src/spring/azext_spring/_validators_enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.")
Expand Down
18 changes: 16 additions & 2 deletions src/spring/azext_spring/api_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
94 changes: 93 additions & 1 deletion src/spring/azext_spring/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -364,3 +380,79 @@ def _route_config_property_convert(raw_json):
replaced_key = re.sub('(?<!^)(?=[A-Z])', '_', key).lower()
convert_raw_json[replaced_key] = raw_json[key]
return convert_raw_json


def _update_response_cache(client, resource_group, service, existing_response_cache=None,
enable_response_cache=None,
response_cache_scope=None,
response_cache_size=None,
response_cache_ttl=None):
if existing_response_cache is None and not enable_response_cache:
if response_cache_scope is not None or response_cache_size is not None or response_cache_ttl is not None:
raise InvalidArgumentValueError("Response cache is not enabled. "
"Please use --enable-response-cache together to configure it.")

if existing_response_cache is None and enable_response_cache:
if response_cache_scope is None:
raise InvalidArgumentValueError("--response-cache-scope is required when enable response cache.")

# enable_response_cache can be None, which can still mean to enable response cache
if enable_response_cache is False:
return None

target_cache_scope = _get_target_cache_scope(response_cache_scope, existing_response_cache)
target_cache_size = _get_target_cache_size(response_cache_size, existing_response_cache)
target_cache_ttl = _get_target_cache_ttl(response_cache_ttl, existing_response_cache)

if target_cache_scope is None:
if target_cache_size is None and target_cache_ttl is None:
return None
else:
raise InvalidArgumentValueError("--response-cache-scope is required when enable response cache.")

if target_cache_scope == GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE:
return models.GatewayLocalResponseCachePerRouteProperties(
size=target_cache_size, time_to_live=target_cache_ttl)
else:
return models.GatewayLocalResponseCachePerInstanceProperties(
size=target_cache_size, time_to_live=target_cache_ttl)


def _get_target_cache_scope(response_cache_scope, existing_response_cache):
if response_cache_scope is not None:
return response_cache_scope

if existing_response_cache is None:
return None

if isinstance(existing_response_cache, models.GatewayLocalResponseCachePerRouteProperties):
return GATEWAY_RESPONSE_CACHE_SCOPE_ROUTE

if isinstance(existing_response_cache, models.GatewayLocalResponseCachePerInstanceProperties):
return GATEWAY_RESPONSE_CACHE_SCOPE_INSTANCE


def _get_target_cache_size(size, existing_response_cache):
if size is not None:
if size == GATEWAY_RESPONSE_CACHE_SIZE_RESET_VALUE:
return None
else:
return size

if existing_response_cache is None or existing_response_cache.size is None:
return None
else:
return existing_response_cache.size


def _get_target_cache_ttl(ttl, existing_response_cache):
if ttl is not None:
if ttl == GATEWAY_RESPONSE_CACHE_TTL_RESET_VALUE:
return None
else:
return ttl

if existing_response_cache is None or existing_response_cache.time_to_live is None:
return None
else:
return existing_response_cache.time_to_live
Loading

0 comments on commit 533e873

Please sign in to comment.