diff --git a/src/spring/HISTORY.md b/src/spring/HISTORY.md index a2209d925c4..615c3f4e077 100644 --- a/src/spring/HISTORY.md +++ b/src/spring/HISTORY.md @@ -1,5 +1,9 @@ Release History =============== +Upcoming +--- +* Add new argument `--server-version` in `az spring app deploy and az spring app deployment create` to support war file deployment in Standard tier. + 1.14.3 --- * Make error message for `az spring app logs` more readable. diff --git a/src/spring/azext_spring/_deployment_deployable_factory.py b/src/spring/azext_spring/_deployment_deployable_factory.py index da7b798d3e4..3dd4a39bbeb 100644 --- a/src/spring/azext_spring/_deployment_deployable_factory.py +++ b/src/spring/azext_spring/_deployment_deployable_factory.py @@ -12,6 +12,7 @@ from ._stream_utils import stream_logs from ._buildservices_factory import BuildService from threading import Timer +from ._utils import (_get_file_ext) logger = get_logger(__name__) @@ -38,11 +39,13 @@ def build_deployable_path(self, **_): def stream_log(self, **_): pass - def get_source_type(self, runtime_version=None, **_): + def get_source_type(self, runtime_version=None, artifact_path=None, **_): if self.sku.name == 'E0': return 'BuildResult' if runtime_version and runtime_version.lower() == 'netcore_31': return 'NetCoreZip' + if _get_file_ext(artifact_path).lower() == ".war": + return "War" return 'Jar' diff --git a/src/spring/azext_spring/_deployment_factory.py b/src/spring/azext_spring/_deployment_factory.py index 0b2298a9c65..2e4dad31127 100644 --- a/src/spring/azext_spring/_deployment_factory.py +++ b/src/spring/azext_spring/_deployment_factory.py @@ -261,6 +261,8 @@ def deployment_source_options_from_resource(original): options['jvm_options'] = original.properties.source.jvm_options if hasattr(original.properties.source, 'runtime_version'): options['runtime_version'] = original.properties.source.runtime_version + if hasattr(original.properties.source, 'server_version'): + options['server_version'] = original.properties.source.server_version return options diff --git a/src/spring/azext_spring/_deployment_source_factory.py b/src/spring/azext_spring/_deployment_source_factory.py index d47ac25488d..7d6b2c80d7c 100644 --- a/src/spring/azext_spring/_deployment_source_factory.py +++ b/src/spring/azext_spring/_deployment_source_factory.py @@ -51,6 +51,40 @@ def fulfilled_options_from_original_source_info(self, deployment_resource, } +class WarSource(BaseSource): + def validate_source(self, **kwargs): + invalid_input = {k: v for k, v in kwargs.items() if k in ['main_entry', 'target_module'] and v is not None} + if any(invalid_input): + invalid_input_str = convert_argument_to_parameter_list(invalid_input.keys()) + runtime_version = kwargs.get('runtime_version') or kwargs.get('deployment_resource').properties.source.runtime_version + raise ArgumentUsageError('{} cannot be set when --runtime-version is {}.' + .format(invalid_input_str, runtime_version)) + + def format_source(self, deployable_path=None, runtime_version=None, server_version=None, version=None, jvm_options=None, **_): + if all(x is None for x in [deployable_path, runtime_version, server_version, version, jvm_options]): + return + return models.WarUploadedUserSourceInfo( + relative_path=deployable_path, + jvm_options=jvm_options, + runtime_version=runtime_version or 'Java_11', + server_version=server_version or 'Tomcat_9', + version=version + ) + + def fulfilled_options_from_original_source_info(self, deployment_resource, + jvm_options=None, runtime_version=None, server_version=None, **_): + if all(x is None for x in [jvm_options, runtime_version]): + return {} + original_source = deployment_resource.properties.source + return { + 'jvm_options': jvm_options if jvm_options is not None else original_source.jvm_options, + 'runtime_version': runtime_version or original_source.runtime_version, + 'server_version': server_version or original_source.server_version, + 'version': original_source.version, + 'deployable_path': original_source.relative_path + } + + class NetCoreZipSource(BaseSource): def validate_source(self, **kwargs): invalid_input = {k: v for k, v in kwargs.items() if k in ['jvm_options'] and v is not None} @@ -163,4 +197,6 @@ def source_selector(source_type=None, **_): return NetCoreZipSource() if source_type == 'BuildResult': return BuildResult() + if source_type == 'War': + return WarSource() return JarSource() diff --git a/src/spring/azext_spring/_help.py b/src/spring/azext_spring/_help.py index 7029a1fddaa..855612795a8 100644 --- a/src/spring/azext_spring/_help.py +++ b/src/spring/azext_spring/_help.py @@ -254,6 +254,8 @@ text: az spring app deploy -n MyApp -s MyCluster -g MyResourceGroup --source-path - name: Deploy a pre-built jar to an app with jvm options and environment variables. text: az spring app deploy -n MyApp -s MyCluster -g MyResourceGroup --artifact-path app.jar --jvm-options="-XX:+UseG1GC -XX:+UseStringDeduplication" --env foo=bar + - name: Deploy a pre-built war to an app with server version, jvm options and environment variables (Standard and Basic Tiers Only). + text: az spring app deploy -n MyApp -s MyCluster -g MyResourceGroup --artifact-path app.war --server-version Tomcat_10 --jvm-options="-XX:+UseG1GC -XX:+UseStringDeduplication" --env foo=bar - name: Deploy source code to a specific deployment of an app. text: az spring app deploy -n MyApp -s MyCluster -g MyResourceGroup -d green-deployment --source-path - name: Deploy a container image on Docker Hub to an app. diff --git a/src/spring/azext_spring/_params.py b/src/spring/azext_spring/_params.py index ed6b9865a7e..6da1fdecb9e 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -16,7 +16,7 @@ validate_ingress_timeout, validate_jar, validate_ingress_send_timeout, validate_ingress_session_max_age, validate_config_server_ssh_or_warn, validate_remote_debugging_port, validate_ingress_client_auth_certificates, - validate_managed_environment, validate_dataplane_public_endpoint) + validate_managed_environment, validate_dataplane_public_endpoint, validate_server_version) from ._validators_enterprise import (only_support_enterprise, validate_builder_resource, validate_builder_create, validate_source_path, validate_artifact_path, validate_build_create, validate_build_update, validate_container_registry_create, @@ -490,7 +490,7 @@ def prepare_logs_argument(c): 'artifact_path', options_list=['--artifact-path', 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) + help='Deploy the specified pre-built artifact (jar, war or netcore zip, war is in public preview).', validator=validate_jar) c.argument( 'disable_validation', arg_type=get_three_state_flag(), help='If true, disable jar validation.') @@ -528,6 +528,7 @@ def prepare_logs_argument(c): c.argument('build_certificates', nargs='*', help='(Enterprise Tier Only) Space-separated certificate names, the certificates are used during build time.', validator=validate_build_cert_reference) + c.argument('server_version', help='(Standard and Basic Tiers Only) Tomcat server version. This feature is in public preview.', validator=validate_server_version) with self.argument_context('spring app deploy') as c: c.argument('source_path', arg_type=source_path_type, validator=validate_deloy_path) diff --git a/src/spring/azext_spring/_utils.py b/src/spring/azext_spring/_utils.py index 13111391d4f..7d3a355712b 100644 --- a/src/spring/azext_spring/_utils.py +++ b/src/spring/azext_spring/_utils.py @@ -40,8 +40,18 @@ def _get_upload_local_file(runtime_version, artifact_path=None, source_path=None return file_type, file_path +def _get_file_ext(artifact_path): + return os.path.splitext(artifact_path)[-1].lower() if artifact_path else "" + + def _get_file_type(runtime_version, artifact_path=None): - file_type = "Jar" if _is_java(runtime_version) else "Others" + file_type = "Others" + if _is_java(runtime_version): + file_ext = _get_file_ext(artifact_path) + if file_ext.lower() == ".jar": + file_type = "Jar" + elif file_ext.lower() == ".war": + file_type = "War" if artifact_path is None: file_type = "Source" return file_type diff --git a/src/spring/azext_spring/_validators.py b/src/spring/azext_spring/_validators.py index 1fbecd0f96d..f47210f8627 100644 --- a/src/spring/azext_spring/_validators.py +++ b/src/spring/azext_spring/_validators.py @@ -755,3 +755,8 @@ def validate_managed_environment(namespace): managed_environment = parse_resource_id(managed_environment_id) if managed_environment['namespace'].lower() != 'microsoft.app' or managed_environment['type'].lower() != 'managedenvironments': raise InvalidArgumentValueError('--managed-environment {0} is not a valid Container App Environment resource ID'.format(managed_environment_id)) + + +def validate_server_version(cmd, namespace): + if namespace.server_version and is_enterprise_tier(cmd, namespace.resource_group, namespace.service): + raise ArgumentUsageError("'--server-version' only supports for Standard/Basic tier Spring instance.") diff --git a/src/spring/azext_spring/app.py b/src/spring/azext_spring/app.py index 066648a45e6..d83b0945031 100644 --- a/src/spring/azext_spring/app.py +++ b/src/spring/azext_spring/app.py @@ -307,6 +307,7 @@ def app_deploy(cmd, client, resource_group, service, name, source_path=None, target_module=None, runtime_version=None, + server_version=None, jvm_options=None, main_entry=None, container_image=None, @@ -362,6 +363,7 @@ def app_deploy(cmd, client, resource_group, service, name, 'apms': apms, 'build_certificates': build_certificates, 'runtime_version': runtime_version, + 'server_version': server_version, 'jvm_options': jvm_options, 'main_entry': main_entry, 'version': version, @@ -523,6 +525,7 @@ def deployment_create(cmd, client, resource_group, service, app, name, source_path=None, target_module=None, runtime_version=None, + server_version=None, jvm_options=None, main_entry=None, container_image=None, @@ -586,6 +589,7 @@ def deployment_create(cmd, client, resource_group, service, app, name, 'apms': apms, 'build_certificates': build_certificates, 'runtime_version': runtime_version, + 'server_version': server_version, 'jvm_options': jvm_options, 'main_entry': main_entry, 'version': version, diff --git a/src/spring/azext_spring/tests/latest/test_asa_app.py b/src/spring/azext_spring/tests/latest/test_asa_app.py index 9776441b761..ee042709434 100644 --- a/src/spring/azext_spring/tests/latest/test_asa_app.py +++ b/src/spring/azext_spring/tests/latest/test_asa_app.py @@ -154,6 +154,17 @@ def test_app_deploy_with_runtime_version(self, file_mock): self.assertIsNone(resource.properties.source.version) self.assertEqual('Java_8', resource.properties.source.runtime_version) + @mock.patch('azext_spring._deployment_uploadable_factory.FileUpload.upload_and_build') + def test_app_deploy_war(self, file_mock): + file_mock.return_value = mock.MagicMock() + self._execute('rg', 'asc', 'app', deployment=self._get_deployment(), artifact_path='my-path/test.war', runtime_version='Java_8', server_version='Tomcat_10') + resource = self.patch_deployment_resource + self.assertEqual('War', resource.properties.source.type) + self.assertEqual('my-relative-path', resource.properties.source.relative_path) + self.assertIsNone(resource.properties.source.version) + self.assertEqual('Java_8', resource.properties.source.runtime_version) + self.assertEqual('Tomcat_10', resource.properties.source.server_version) + @mock.patch('azext_spring._deployment_uploadable_factory.FileUpload.upload_and_build') def test_app_deploy_net(self, file_mock): file_mock.return_value = mock.MagicMock() @@ -400,6 +411,26 @@ def test_app_deploy_container_from_jar(self): self.assertEqual('2Gi', resource.properties.deployment_settings.resource_requests.memory) self.assertEqual(2, resource.sku.capacity) + @mock.patch('azext_spring._deployment_uploadable_factory.FileUpload.upload_and_build') + def test_app_deploy_war_from_container(self, file_mock): + file_mock.return_value = mock.MagicMock() + deployment = self._get_deployment() + deployment.properties.source.type = 'Container' + deployment.properties.source.custom_container = mock.MagicMock() + deployment.properties.source.relative_path = None + deployment.properties.source.runtime_version = None + deployment.properties.source.version = '123' + self._execute('rg', 'asc', 'app', deployment=deployment, artifact_path='my-path/test.war', server_version='Tomcat_9') + resource = self.put_deployment_resource + self.assertEqual('War', resource.properties.source.type) + self.assertEqual('my-relative-path', resource.properties.source.relative_path) + self.assertIsNone(resource.properties.source.version) + self.assertEqual('Java_11', resource.properties.source.runtime_version) + self.assertEqual('Tomcat_9', resource.properties.source.server_version) + self.assertEqual('2', resource.properties.deployment_settings.resource_requests.cpu) + self.assertEqual('2Gi', resource.properties.deployment_settings.resource_requests.memory) + self.assertEqual(2, resource.sku.capacity) + @mock.patch('azext_spring._deployment_uploadable_factory.FileUpload.upload_and_build') def test_app_deploy_jar_from_container(self, file_mock): file_mock.return_value = mock.MagicMock()