Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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