From 5fa9c478e4ad57e09b2c2a220b989526966b62ac Mon Sep 17 00:00:00 2001 From: Yuwei Zhou Date: Tue, 28 Dec 2021 09:46:07 +0800 Subject: [PATCH] [spring-cloud] Add validator for app deploy and app deployment create (#4258) * Add validator for app deploy/create * fix lint * fix error message --- .../azext_spring_cloud/_app_validator.py | 27 +++++++ .../azext_spring_cloud/_params.py | 46 +++++++----- .../tests/latest/test_asc_app_validator.py | 71 ++++++++++++++++++- 3 files changed, 123 insertions(+), 21 deletions(-) diff --git a/src/spring-cloud/azext_spring_cloud/_app_validator.py b/src/spring-cloud/azext_spring_cloud/_app_validator.py index 6df82949ba8..2988ecc82c6 100644 --- a/src/spring-cloud/azext_spring_cloud/_app_validator.py +++ b/src/spring-cloud/azext_spring_cloud/_app_validator.py @@ -9,6 +9,7 @@ from msrestazure.azure_exceptions import CloudError from azure.core.exceptions import (ResourceNotFoundError) from ._client_factory import cf_spring_cloud +from ._resource_quantity import (validate_cpu as validate_cpu_value, validate_memory as validate_memory_value) # pylint: disable=line-too-long,raise-missing-from @@ -76,3 +77,29 @@ def _get_active_deployment(client, resource_group, service, name): return next(iter(x for x in deployments if x.properties.active), None) except ResourceNotFoundError: raise InvalidArgumentValueError('Deployments not found under App {}'.format(name)) + + +def validate_deloy_path(namespace): + arguments = [namespace.artifact_path, namespace.source_path, namespace.container_image] + if all(not x for x in arguments): + raise InvalidArgumentValueError('One of --artifact-path, --source-path, --container-image must be provided.') + _deploy_path_mutual_exclusive(arguments) + + +def validate_deloyment_create_path(namespace): + arguments = [namespace.artifact_path, namespace.source_path, namespace.container_image] + _deploy_path_mutual_exclusive(arguments) + + +def _deploy_path_mutual_exclusive(args): + valued_args = [x for x in args if x] + if len(valued_args) > 1: + raise InvalidArgumentValueError('At most one of --artifact-path, --source-path, --container-image must be provided.') + + +def validate_cpu(namespace): + namespace.cpu = validate_cpu_value(namespace.cpu) + + +def validate_memory(namespace): + namespace.memory = validate_memory_value(namespace.memory) diff --git a/src/spring-cloud/azext_spring_cloud/_params.py b/src/spring-cloud/azext_spring_cloud/_params.py index bf70b09eb66..311062f5ec8 100644 --- a/src/spring-cloud/azext_spring_cloud/_params.py +++ b/src/spring-cloud/azext_spring_cloud/_params.py @@ -15,7 +15,8 @@ validate_app_insights_parameters, validate_instance_count, validate_java_agent_parameters, validate_jar) from ._app_validator import (fulfill_deployment_param, active_deployment_exist, active_deployment_exist_under_app, - ensure_not_active_deployment) + ensure_not_active_deployment, validate_deloy_path, validate_deloyment_create_path, + validate_cpu, validate_memory) from ._utils import ApiType from .vendored_sdks.appplatform.v2020_07_01.models import RuntimeVersion, TestKeyType @@ -27,6 +28,12 @@ service_name_type = CLIArgumentType(options_list=['--service', '-s'], help='Name of Azure Spring Cloud, you can configure the default service using az configure --defaults spring-cloud=.', configured_default='spring-cloud') app_name_type = CLIArgumentType(help='App name, you can configure the default app using az configure --defaults spring-cloud-app=.', validator=validate_app_name, configured_default='spring-cloud-app') sku_type = CLIArgumentType(arg_type=get_enum_type(['Basic', 'Standard', 'Enterprise']), validator=validate_sku, help='Name of SKU. Enterprise is still in Preview.') +source_path_type = CLIArgumentType(nargs='?', const='.', + help="Deploy the specified source folder. The folder will be packed into tar, uploaded, and built using kpack. Default to the current folder if no value provided.", + arg_group='Source Code deploy') +# app cpu and memory +cpu_type = CLIArgumentType(type=str, help='CPU resource quantity. Should be 500m or number of CPU cores.', validator=validate_cpu) +memort_type = CLIArgumentType(type=str, help='Memory resource quantity. Should be 512Mi or #Gi, e.g., 1Gi, 3Gi.', validator=validate_memory) # pylint: disable=too-many-statements @@ -122,10 +129,8 @@ def load_arguments(self, _): options_list=['--assign-endpoint', c.deprecate(target='--is-public', redirect='--assign-endpoint', hide=True)]) c.argument('assign_identity', arg_type=get_three_state_flag(), help='If true, assign managed service identity.') - c.argument('cpu', type=str, default="1", - help='CPU resource quantity. Should be 500m or number of CPU cores.') - c.argument('memory', type=str, default="1Gi", - help='Memory resource quantity. Should be 512Mi or #Gi, e.g., 1Gi, 3Gi.') + c.argument('cpu', arg_type=cpu_type, default="1") + c.argument('memory', arg_type=memort_type, default="1Gi") c.argument('instance_count', type=int, default=1, help='Number of instance.', validator=validate_instance_count) c.argument('persistent_storage', type=str, @@ -207,8 +212,8 @@ def prepare_logs_argument(c): c.argument('disable_probe', arg_type=get_three_state_flag(), help='If true, disable the liveness and readiness probe.') with self.argument_context('spring-cloud app scale') as c: - c.argument('cpu', type=str, help='CPU resource quantity. Should be 500m or number of CPU cores.') - c.argument('memory', type=str, help='Memory resource quantity. Should be 512Mi or #Gi, e.g., 1Gi, 3Gi.') + c.argument('cpu', arg_type=cpu_type) + c.argument('memory', arg_type=memort_type) c.argument('instance_count', type=int, help='Number of instance.', validator=validate_instance_count) for scope in ['spring-cloud app deploy', 'spring-cloud app deployment create']: @@ -218,9 +223,6 @@ def prepare_logs_argument(c): c.deprecate(target='--jar-path', redirect='--artifact-path', hide=True), c.deprecate(target='-p', redirect='--artifact-path', hide=True)], help='Deploy the specified pre-built artifact (jar or netcore zip).', validator=validate_jar) - c.argument( - 'source_path', nargs='?', const='.', - help="Deploy the specified source folder. The folder will be packed into tar, uploaded, and built using kpack. Default to the current folder if no value provided.") c.argument( 'disable_validation', arg_type=get_three_state_flag(), help='If true, disable jar validation.') @@ -228,27 +230,33 @@ def prepare_logs_argument(c): 'main_entry', options_list=[ '--main-entry', '-m'], help="A string containing the path to the .NET executable relative to zip root.") c.argument( - 'target_module', help='Child module to be deployed, required for multiple jar packages built from source code.') + 'target_module', help='Child module to be deployed, required for multiple jar packages built from source code.', arg_group='Source Code deploy') c.argument( 'version', help='Deployment version, keep unchanged if not set.') c.argument( - 'container_image', help='The container image tag.') + 'container_image', help='The container image tag.', arg_group='Custom Container') c.argument( - 'container_registry', default='docker.io', help='The registry of the container image.') + 'container_registry', default='docker.io', help='The registry of the container image.', arg_group='Custom Container') c.argument( - 'registry_username', help='The username of the container registry.') + 'registry_username', help='The username of the container registry.', arg_group='Custom Container') c.argument( - 'registry_password', help='The password of the container registry.') + 'registry_password', help='The password of the container registry.', arg_group='Custom Container') c.argument( - 'container_command', help='The command of the container image.') + 'container_command', help='The command of the container image.', arg_group='Custom Container') c.argument( - 'container_args', help='The arguments of the container image.') + 'container_args', help='The arguments of the container image.', arg_group='Custom Container') + + with self.argument_context('spring-cloud app deploy') as c: + c.argument('source_path', arg_type=source_path_type, validator=validate_deloy_path) + + with self.argument_context('spring-cloud app deployment create') as c: + c.argument('source_path', arg_type=source_path_type, validator=validate_deloyment_create_path) with self.argument_context('spring-cloud app deployment create') as c: c.argument('skip_clone_settings', help='Create staging deployment will automatically copy settings from production deployment.', action='store_true') - c.argument('cpu', type=str, help='CPU resource quantity. Should be 500m or number of CPU cores.') - c.argument('memory', type=str, help='Memory resource quantity. Should be 512Mi or #Gi, e.g., 1Gi, 3Gi.') + c.argument('cpu', arg_type=cpu_type) + c.argument('memory', arg_type=memort_type) c.argument('instance_count', type=int, help='Number of instance.', validator=validate_instance_count) with self.argument_context('spring-cloud app deployment') as c: diff --git a/src/spring-cloud/azext_spring_cloud/tests/latest/test_asc_app_validator.py b/src/spring-cloud/azext_spring_cloud/tests/latest/test_asc_app_validator.py index 20c10732a97..b6ab0ce4553 100644 --- a/src/spring-cloud/azext_spring_cloud/tests/latest/test_asc_app_validator.py +++ b/src/spring-cloud/azext_spring_cloud/tests/latest/test_asc_app_validator.py @@ -3,12 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import unittest -import copy from argparse import Namespace from azure.cli.core.azclierror import InvalidArgumentValueError from msrestazure.azure_exceptions import CloudError from azure.core.exceptions import ResourceNotFoundError -from ..._app_validator import (fulfill_deployment_param, active_deployment_exist, active_deployment_exist_under_app) +from ..._app_validator import (fulfill_deployment_param, active_deployment_exist, active_deployment_exist_under_app, + validate_cpu, validate_memory, validate_deloyment_create_path, validate_deloy_path) try: @@ -39,6 +39,73 @@ def _get_deployment(resource_group, service, app, deployment, active): return resource +class TestCpuAndMemoryValidator(unittest.TestCase): + def test_none_input(self): + ns = Namespace(cpu=None, memory=None) + validate_memory(ns) + validate_cpu(ns) + self.assertIsNone(ns.cpu) + self.assertIsNone(ns.memory) + + def test_int_input(self): + ns = Namespace(cpu='1', memory='1') + validate_memory(ns) + validate_cpu(ns) + self.assertEqual('1', ns.cpu) + self.assertEqual('1Gi', ns.memory) + + def test_str_input(self): + ns = Namespace(cpu='1', memory='1Gi') + validate_memory(ns) + validate_cpu(ns) + self.assertEqual('1', ns.cpu) + self.assertEqual('1Gi', ns.memory) + + def test_invalid_memory(self): + ns = Namespace(memory='invalid') + with self.assertRaises(InvalidArgumentValueError) as context: + validate_memory(ns) + self.assertEqual('Memory quantity should be integer followed by unit (Mi/Gi)', str(context.exception)) + + def test_invalid_cpu(self): + ns = Namespace(cpu='invalid') + with self.assertRaises(InvalidArgumentValueError) as context: + validate_cpu(ns) + self.assertEqual('CPU quantity should be millis (500m) or integer (1, 2, ...)', str(context.exception)) + + +class TestDeployPath(unittest.TestCase): + def test_no_deploy_path_provided_when_create(self): + ns = Namespace(source_path=None, artifact_path=None, container_image=None) + validate_deloyment_create_path(ns) + + def test_no_deploy_path_when_deploy(self): + ns = Namespace(source_path=None, artifact_path=None, container_image=None) + with self.assertRaises(InvalidArgumentValueError): + validate_deloy_path(ns) + + def test_more_than_one_path(self): + ns = Namespace(source_path='test', artifact_path='test', container_image=None) + with self.assertRaises(InvalidArgumentValueError): + validate_deloy_path(ns) + with self.assertRaises(InvalidArgumentValueError): + validate_deloyment_create_path(ns) + + def test_more_than_one_path_1(self): + ns = Namespace(source_path='test', artifact_path='test', container_image='test') + with self.assertRaises(InvalidArgumentValueError): + validate_deloy_path(ns) + with self.assertRaises(InvalidArgumentValueError): + validate_deloyment_create_path(ns) + + def test_more_than_one_path_2(self): + ns = Namespace(source_path='test', artifact_path=None, container_image='test') + with self.assertRaises(InvalidArgumentValueError): + validate_deloy_path(ns) + with self.assertRaises(InvalidArgumentValueError): + validate_deloyment_create_path(ns) + + class TestActiveDeploymentExist(unittest.TestCase): @mock.patch('azext_spring_cloud._app_validator.cf_spring_cloud', autospec=True) def test_deployment_found(self, client_factory_mock):