diff --git a/src/spring/HISTORY.md b/src/spring/HISTORY.md index 7c588a7414d..7fd31a0051b 100644 --- a/src/spring/HISTORY.md +++ b/src/spring/HISTORY.md @@ -1,5 +1,13 @@ Release History =============== +1.27.1 +--- +* Support scenario of bringing your own container image for command `az spring export`. + +1.27.0 +--- +* Add command `az spring export` which is used to generate target resources definitions to help customer migrating from Azure Spring Apps to other Azure services, such as Azure Container Apps. + 1.26.1 --- * Fix command `az spring app update`, so that it can detect update failure and return error message. diff --git a/src/spring/azext_spring/_help.py b/src/spring/azext_spring/_help.py index 7e04a6e4ff8..b68e8f444e1 100644 --- a/src/spring/azext_spring/_help.py +++ b/src/spring/azext_spring/_help.py @@ -1809,3 +1809,11 @@ - name: Clean up private DNS zone with Azure Spring Apps. text: az spring private-dns-zone clean --service MyAzureSpringAppsInstance --resource-group MyResourceGroup """ + +helps['spring export'] = """ + type: command + short-summary: Commands to export target Azure resource definitions from Azure Spring Apps. + examples: + - name: Generate corresponding bicep files and README doc to create Azure Container Apps service. + text: az spring export --target aca --service MyAzureSpringAppsInstance --resource-group MyResourceGroup --output-folder output +""" diff --git a/src/spring/azext_spring/_params.py b/src/spring/azext_spring/_params.py index 57467910ac8..eafc579e898 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -1307,3 +1307,8 @@ def prepare_common_logs_argument(c): c.argument('zone_id', help='The resource id of the private DNS zone which you would like to configure with the service instance.') with self.argument_context('spring private-dns-zone clean') as c: c.argument('service', service_name_type) + + with self.argument_context('spring export') as c: + c.argument('service', service_name_type) + c.argument('target', arg_type=get_enum_type(["aca", "azure-container-apps"]), help='The target Azure service to migrate to.') + c.argument('output_folder', help='The output folder for the generated Bicep files.') diff --git a/src/spring/azext_spring/commands.py b/src/spring/azext_spring/commands.py index 5d75c5c5a53..64f59834c1f 100644 --- a/src/spring/azext_spring/commands.py +++ b/src/spring/azext_spring/commands.py @@ -5,6 +5,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType +from azure.cli.core.profiles import ResourceType from azext_spring._utils import handle_asc_exception from ._client_factory import (cf_spring, @@ -519,5 +520,9 @@ def load_command_table(self, _): exception_handler=handle_asc_exception, is_preview=True) as g: g.custom_command('list', 'job_execution_instance_list', validator=job_validators.validate_job_execution_instance_list) + with self.command_group('spring', custom_command_type=spring_routing_util, resource_type=ResourceType.MGMT_RESOURCE_RESOURCES, + exception_handler=handle_asc_exception, is_preview=True) as g: + g.custom_command('export', 'spring_migration_start') + with self.command_group('spring', exception_handler=handle_asc_exception): pass diff --git a/src/spring/azext_spring/migration/__init__.py b/src/spring/azext_spring/migration/__init__.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/spring/azext_spring/migration/__init__.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/spring/azext_spring/migration/converter/__init__.py b/src/spring/azext_spring/migration/converter/__init__.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/spring/azext_spring/migration/converter/__init__.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py new file mode 100644 index 00000000000..b2dfdf98261 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -0,0 +1,96 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.log import get_logger +from .base_converter import BaseConverter + +logger = get_logger(__name__) + + +# Concrete Converter Subclass for Config Server +class ACSConverter(BaseConverter): + + CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" + KEY_URI = ".uri" + KEY_LABEL = ".default-label" + KEY_SEARCH_PATHS = ".search-paths" + KEY_USERNAME = ".username" + KEY_PASSWORD = ".password" + KEY_PRIVATE_KEY = ".private-key" + KEY_HOST_KEY = ".host-key" + KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm" + KEY_PATTERN = ".pattern" + + def __init__(self, source): + def transform_data(): + if self.wrapper_data.is_support_ossconfigserver() is False and self.wrapper_data.is_support_acs(): + acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0] + name = "config" + configurations, params = self._get_configurations_and_params(acs) + replicas = 2 + return { + "configServerName": name, + "params": params, + "configurations": configurations, + "replicas": replicas + } + else: + return None + super().__init__(source, transform_data) + + def get_template_name(self): + return "config_server.bicep" + + def _get_configurations_and_params(self, acs): + configurations = [] + params = [] + + git_repos = acs.get('properties', {}).get('settings', {}).get('gitProperty', {}).get('repositories', None) + if git_repos is not None and len(git_repos) > 0: + default_repo = git_repos[0] + self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, default_repo.get('uri')) + self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, default_repo.get('label')) + self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, default_repo.get('searchPaths')) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, default_repo.get('username'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, default_repo.get('password'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, default_repo.get('privateKey'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, default_repo.get('hostKey'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, default_repo.get('hostKeyAlgorithm'), configurations, params) + self._check_patterns(default_repo) + + for i in range(1, len(git_repos)): + repo = git_repos[i] + configuration_key_repo_prefix = self.CONFIGURATION_KEY_PREFIX + ".repos." + repo['name'] + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm'), configurations, params) + self._check_patterns(repo) + + return configurations, params + + def _add_property_if_exists(self, configurations, key, value): + if value: + if isinstance(value, (list, tuple)): + value = ",".join(map(str, value)) + configurations.append({ + "propertyName": key, + "value": value + }) + + def _add_secret_config(self, key, value, configurations, params): + if value: + param_name = key.replace(".", "_").replace("-", "_") + self._add_property_if_exists(configurations, key, param_name) + params.append(param_name) + + def _check_patterns(self, repo): + patterns = repo.get('patterns', []) + if len(patterns) > 0: + pattern_str = ",".join(map(str, patterns)) + logger.info(f"The patterns '{pattern_str}' of the git repository '{repo.get('name')}' in Application Configuration Service not need in Config Server of Azure Container Apps.") diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py new file mode 100644 index 00000000000..cbb6226626d --- /dev/null +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -0,0 +1,286 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.log import get_logger +from .base_converter import BaseConverter + +logger = get_logger(__name__) + + +# Concrete Converter Subclass for Container App +class AppConverter(BaseConverter): + + DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" + + def __init__(self, source): + def transform_data(): + return self.wrapper_data.get_apps() + super().__init__(source, transform_data) + + def transform_data_item(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + blueDeployment = self._transform_deployment(blueDeployment) + maxReplicas = blueDeployment.get("capacity", 5) if blueDeployment is not None else 5 + greenDeployment = self.wrapper_data.get_green_deployment_by_app(app) + greenDeployment = self._transform_deployment(greenDeployment) + if self.wrapper_data.is_support_blue_green_deployment(app): + logger.warning(f"Action Needed: you should manually deploy the deployment '{greenDeployment.get('name')}' of app '{app.get('name')}' in Azure Container Apps.") + tier = blueDeployment.get('sku', {}).get('tier') if blueDeployment is not None else 'Standard' + serviceBinds = self._get_service_bind(app) + ingress = self._get_ingress(app, tier) + isPublic = app['properties'].get('public') + identity = app.get('identity') + volumeMounts = [] + volumes = [] + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] + for disk_props in disks: + volume_name = self._get_storage_name(disk_props) + volumeMounts.append({ + "volumeName": volume_name, + "mountPath": self._get_storage_mount_path(disk_props), + }) + volumes.append({ + "volumeName": volume_name, + "storageName": self._get_storage_unique_name(disk_props), + "mountOptions": self._get_mount_options(disk_props), + }) + return { + "isByoc": self.wrapper_data.is_support_custom_container_image_for_app(app), + "isPrivateImage": self.wrapper_data.is_private_custom_container_image(app), + "paramContainerAppImagePassword": self._get_param_name_of_container_image_password(app), + "containerRegistry": self._get_container_registry(app), + "args": self._get_args(app), + "commands": self._get_commands(app), + "containerAppName": self._get_resource_name(app), + "paramContainerAppImageName": self._get_param_name_of_container_image(app), + "paramTargetPort": self._get_param_name_of_target_port(app), + "moduleName": self._get_app_module_name(app), + "ingress": ingress, + "isPublic": isPublic, + "minReplicas": 1, + "maxReplicas": maxReplicas, + "serviceBinds": serviceBinds, + "blue": blueDeployment, + "green": greenDeployment, + "isBlueGreen": self.wrapper_data.is_support_blue_green_deployment(app), + "identity": identity, + "volumeMounts": volumeMounts, + "volumes": volumes, + } + + def get_template_name(self): + return "app.bicep" + + def _get_service_bind(self, app): + service_bind = [] + envName = self._get_parent_resource_name(app) + if self.wrapper_data.is_support_configserver_for_app(app): + service_bind.append({ + "name": "bind-config", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" + }) + if self.wrapper_data.is_enterprise_tier() is not True and self.wrapper_data.is_support_ossconfigserver(): + # standard tier enabled config server and bind all apps automatically + service_bind.append({ + "name": "bind-config", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" + }) + if self.wrapper_data.is_support_serviceregistry_for_app(app): + service_bind.append({ + "name": "bind-eureka", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" + }) + if self.wrapper_data.is_enterprise_tier() is not True and self.wrapper_data.is_support_eureka(): + # standard tier enabled eureka server and bind all apps automatically + service_bind.append({ + "name": "bind-eureka", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" + }) + if self.wrapper_data.is_support_sba(): + service_bind.append({ + "name": "bind-sba", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'admin')" + }) + return service_bind + + def _transform_deployment(self, deployment): + if deployment is None: + return + env = deployment.get('properties', {}).get('deploymentSettings', {}).get('environmentVariables', {}) + liveness_probe = deployment.get('properties', {}).get('deploymentSettings', {}).get('livenessProbe', {}) + readiness_probe = deployment.get('properties', {}).get('deploymentSettings', {}).get('readinessProbe', {}) + startup_probe = deployment.get('properties', {}).get('deploymentSettings', {}).get('startupProbe', {}) + resource_requests = deployment.get('properties', {}).get('deploymentSettings', {}).get('resourceRequests', {}) + cpuCore = float(resource_requests.get("cpu").replace("250m", "0.25").replace("500m", "0.5")) + memorySize = resource_requests.get("memory") + tier = deployment.get('sku', {}).get('tier') + scale = deployment.get('properties', {}).get('deploymentSettings', {}).get('scale', {}) + capacity = deployment.get('sku', {}).get('capacity') + return { + "name": self._get_resource_name(deployment), + "env": self._convert_env(env), + "livenessProbe": self._convert_probe(liveness_probe, tier, deployment), + "readinessProbe": self._convert_probe(readiness_probe, tier, deployment), + "startupProbe": self._convert_probe(startup_probe, tier, deployment), + "cpuCore": cpuCore, + "memorySize": self._get_memory_by_cpu(cpuCore, memorySize, deployment) or memorySize, + "scale": self._convert_scale(scale), + "capacity": capacity, + } + + def _convert_env(self, env): + env_list = [] + for key, value in env.items(): + env_list.append({ + "name": key, + "value": value + }) + return env_list + + # A Container App must add up to one of the following CPU - Memory combinations: + # [cpu: 0.25, memory: 0.5Gi]; [cpu: 0.5, memory: 1.0Gi]; [cpu: 0.75, memory: 1.5Gi]; [cpu: 1.0, memory: 2.0Gi]; [cpu: 1.25, memory: 2.5Gi]; [cpu: 1.5, memory: 3.0Gi]; [cpu: 1.75, memory: 3.5Gi]; [cpu: 2.0, memory: 4.0Gi]; [cpu: 2.25, memory: 4.5Gi]; [cpu: 2.5, memory: 5.0Gi]; [cpu: 2.75, memory: 5.5Gi]; [cpu: 3, memory: 6.0Gi]; [cpu: 3.25, memory: 6.5Gi]; [cpu: 3.5, memory: 7Gi]; [cpu: 3.75, memory: 7.5Gi]; [cpu: 4, memory: 8Gi] + def _get_memory_by_cpu(self, cpu, asa_memory_size, deployment): + cpu_memory_map = { + 0.25: "0.5Gi", + 0.5: "1.0Gi", + 0.75: "1.5Gi", + 1.0: "2.0Gi", + 1.25: "2.5Gi", + 1.5: "3.0Gi", + 1.75: "3.5Gi", + 2.0: "4.0Gi", + 2.25: "4.5Gi", + 2.5: "5.0Gi", + 2.75: "5.5Gi", + 3.0: "6.0Gi", + 3.25: "6.5Gi", + 3.5: "7.0Gi", + 3.75: "7.5Gi", + 4.0: "8.0Gi" + } + if cpu_memory_map.get(cpu, None) is None: + logger.warning(f"Mismatch: The CPU '{cpu}' and Memory '{asa_memory_size}' combination of app '{deployment.get('name')}' is not supported in Azure Container Apps.") + elif asa_memory_size != cpu_memory_map.get(cpu, None): + logger.warning(f"Mismatch: The Memory '{asa_memory_size}' of app '{deployment.get('name')}' is not supported in Azure Container Apps. Converting it to '{cpu_memory_map.get(cpu, None)}'.") + + return cpu_memory_map.get(cpu, None) + + # create a method _convert_probe to convert the probe from the source to the target format + def _convert_probe(self, probe, tier, deployment): + if probe is None: + return None + if probe.get("disableProbe") is True: + logger.debug("Probe is disabled") + return None + result = {} + initialDelaySeconds = probe.get("initialDelaySeconds", None) + if initialDelaySeconds is not None: + if initialDelaySeconds > 60: # Container 'undefined' 'Type' probe's InitialDelaySeconds must be in the range of ['0', '60']. + logger.warning(f"Mismatch: The initialDelaySeconds '{initialDelaySeconds}' of health probe of app '{deployment.get('name')}' must be in the range of ['0', '60'] in Azure Container Apps. Converting it to 60.") + initialDelaySeconds = 60 + result['initialDelaySeconds'] = initialDelaySeconds + periodSeconds = probe.get("periodSeconds", None) + if periodSeconds is not None: + result['periodSeconds'] = periodSeconds + timeoutSeconds = probe.get("timeoutSeconds", None) + if timeoutSeconds is not None: + result['timeoutSeconds'] = timeoutSeconds + successThreshold = probe.get("successThreshold", None) + if successThreshold is not None: + result['successThreshold'] = successThreshold + failureThreshold = probe.get("failureThreshold", None) + if failureThreshold is not None: + result['failureThreshold'] = failureThreshold + httpGet = self._convert_http_probe_action(probe, tier) + if httpGet is not None: + result["httpGet"] = httpGet + tcpSocket = self._convert_tcp_probe_action(probe, tier) + if tcpSocket is not None: + result["tcpSocket"] = tcpSocket + execAction = self._convert_exec_probe_action(probe, tier) + if execAction is not None: + logger.warning(f"Mismatch: The ExecAction '{execAction}' of health probe is not supported in Azure Container Apps.") + return None if result == {} else result + + def _convert_exec_probe_action(self, probe, tier): + probeAction = {} + if probe.get("probeAction", {}).get("type") == "ExecAction": + probeAction = { + "command": probe.get("probeAction", {}).get("command"), + } + else: + probeAction = None + return probeAction + + def _convert_tcp_probe_action(self, probe, tier): + probeAction = {} + if probe.get("probeAction", {}).get("type") == "TCPSocketAction": + probeAction = { + "port": 8080 if tier == "Enterprise" else 1025, + } + else: + probeAction = None + return probeAction + + def _convert_http_probe_action(self, probe, tier): + probeAction = {} + if probe.get("probeAction", {}).get("type") == "HTTPGetAction": + probeAction = { + "scheme": probe.get("probeAction", {}).get("scheme"), + "port": 8080 if tier == "Enterprise" else 1025, + "path": probe.get("probeAction", {}).get("path"), + } + else: + probeAction = None + return probeAction + + def _get_ingress(self, app, tier): + ingress = app['properties'].get('ingressSettings') + if ingress is None: + return None + transport = ingress.get('backendProtocol') + match transport: + case "Default": + transport = "auto" + case _: + transport = "auto" + logger.warning(f"Mismatch: The backendProtocol '{transport}' of app '{app.get('name')}' is not supported in Azure Container Apps. Converting it to 'auto'.") + return { + "targetPort": 8080 if tier == "Enterprise" else 1025, + "transport": transport, + "sessionAffinity": ingress.get('sessionAffinity').replace("Cookie", "sticky").replace("None", "none") + } + + def _convert_scale(self, scale): + return { + "minReplicas": scale.get("minReplicas", 1), + "maxReplicas": scale.get("maxReplicas", 5), + "rules": scale.get("rules", []) + } + + def _get_container_registry(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None and self.wrapper_data.is_support_custom_container_image_for_deployment(blueDeployment): + server = blueDeployment['properties']['source'].get('customContainer').get('server', None) + languageFramework = blueDeployment['properties']['source'].get('customContainer').get('languageFramework', None) + username = blueDeployment['properties']['source'].get('customContainer').get('imageRegistryCredential', {}).get('username', None) + passwordSecretRefPerfix = server.replace(".", "") + passwordSecretRef = f"{passwordSecretRefPerfix}-{username}" + return { + "server": server, + "username": username, + "passwordSecretRef": passwordSecretRef, + "image": self._get_container_image(app), + } + + def _get_args(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None and self.wrapper_data.is_support_custom_container_image_for_deployment(blueDeployment): + return blueDeployment['properties']['source'].get('customContainer').get('args', []) + + def _get_commands(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None and self.wrapper_data.is_support_custom_container_image_for_deployment(blueDeployment): + return blueDeployment['properties']['source'].get('customContainer').get('command', []) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py new file mode 100644 index 00000000000..ff860f81c27 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -0,0 +1,355 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import hashlib +import os + +from knack.log import get_logger +from abc import ABC, abstractmethod +from jinja2 import Template + +logger = get_logger(__name__) + + +# Abstract Base Class for Converter +# The converter is a template class that defines the structure of the conversion process +# The responsibility of the converter is to convert the source data into the output data +class ConverterTemplate(ABC): + def __init__(self, source, transform_data): + self.wrapper_data = SourceDataWrapper(source) + self.transform_data = transform_data + + def convert(self): + result = {} + self.data = self.transform_data() + if (isinstance(self.data, list)): + result = self._convert_many() + # for key in result.keys(): + # logger.debug(f"converted contents of {self.__class__.__name__} for {key}:\n{result.get(key)}") + else: + result = self._convert_one() + # logger.debug(f"converted contents of {__class__.__name__}:\n{result.get(self.get_template_name())}") + return result + + def _convert_one(self): + outputs = {} + if self.data is not None and isinstance(self.data, dict): + outputs[self.get_template_name()] = self.generate_output(self.data) + return outputs + + def _convert_many(self): + outputs = {} + if self.data is not None and isinstance(self.data, list) and len(self.data) > 0: + for item in self.data: + name = item['name'].split('/')[-1] + data = self.transform_data_item(item) + outputs[name + "_" + self.get_template_name()] = self.generate_output(data) + return outputs + + @abstractmethod + def get_template_name(self): + pass + + def generate_output(self, data): + script_dir = os.path.dirname(os.path.abspath(__file__)) + template_name = self.get_template_name().lower() + with open(f"{script_dir}/templates/{template_name}.j2") as file: + template = Template(file.read()) + return template.render(data=data) + + +# Base Converter Class +# The BaseConverter class provides common utility methods that can be used by all concrete converter classes +class BaseConverter(ConverterTemplate): + + # common + def _get_resource_name(self, resource): + return resource['name'].split('/')[-1] + + def _get_parent_resource_name(self, resource): + parts = resource['name'].split('/') + return parts[-2] if len(parts) > 1 else '' + + # Extracts the resource name from a resource ID string in Azure ARM template format + # Format: [resourceId('Microsoft.AppPlatform/Spring/', '', '')] + # Example input: [resourceId('Microsoft.AppPlatform/Spring/storages', 'sample-service', 'storage1')] + # Returns: 'storage1' + def _get_name_from_resource_id(self, resource_id): + # Extract content between square brackets + content = resource_id.strip('[]').strip('resourceId()') + # Split by comma and get the last parameter + params = content.split(',') + # Return the last parameter stripped of quotes and whitespace + result = params[-1].strip().strip("'") if params else '' + # print(f"Resource name: {result}") + return result + +# storage + def _get_storage_name(self, disk_props): + storage_id = disk_props.get('storageId', '') + return self._get_name_from_resource_id(storage_id) if storage_id else '' + + def _get_storage_mount_path(self, disk_props): + return disk_props.get('customPersistentDiskProperties').get('mountPath') + + def _get_storage_share_name(self, disk_props): + return disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + + def _get_storage_access_mode(self, disk_props): + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + return 'ReadOnly' if readOnly else 'ReadWrite' + + def _get_storage_account_name(self, disk_props): + storages = self.wrapper_data.get_storages() + storage_map = { + self._get_resource_name(storage): storage['properties']['accountName'] + for storage in storages + } + storage_name = self._get_storage_name(disk_props) + return storage_map.get(storage_name, '') + + def _get_storage_unique_name(self, disk_props): + storage_name = self._get_storage_name(disk_props) + account_name = self._get_storage_account_name(disk_props) + share_name = self._get_storage_share_name(disk_props) + mount_path = self._get_storage_mount_path(disk_props) + access_mode = self._get_storage_access_mode(disk_props) + storage_unique_name = f"{storage_name}|{account_name}|{share_name}|{mount_path}|{access_mode}" + hash_value = hashlib.md5(storage_unique_name.encode()).hexdigest()[:16] # Take first 16 chars of hash + result = f"{storage_name}{hash_value}".replace("-", "").replace("_", "") + return result[:32] # Ensure total length is no more than 32 + + def _get_mount_options(self, disk_props): + mountOptions = self.DEFAULT_MOUNT_OPTIONS + if disk_props.get('customPersistentDiskProperties').get('mountOptions') is not None \ + and len(disk_props.get('customPersistentDiskProperties').get('mountOptions')) > 0: + mountOptions = "" + for option in disk_props.get('customPersistentDiskProperties').get('mountOptions'): + mountOptions += ("," if mountOptions != "" else "") + option + # print("Mount options: ", mountOptions) + return mountOptions + + def _get_storage_enable_subpath(self, disk_props): + enableSubPath = disk_props.get('customPersistentDiskProperties', False).get('enableSubPath', False) + return enableSubPath + + def _get_app_storage_configs(self): + storage_configs = [] + apps = self.wrapper_data.get_apps() + for app in apps: + # Check if app has properties and customPersistentDiskProperties + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties'].get('customPersistentDisks', []) + for disk_props in disks: + if self._get_storage_enable_subpath(disk_props) is True: + logger.warning("Mismatch: enableSubPath of custom persistent disks is not supported in Azure Container Apps.") + # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) + storage_config = { + 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), + 'storageName': self._get_storage_unique_name(disk_props), + 'shareName': self._get_storage_share_name(disk_props), + 'accessMode': self._get_storage_access_mode(disk_props), + 'accountName': self._get_storage_account_name(disk_props), + } + if storage_config not in storage_configs: + storage_configs.append(storage_config) + return storage_configs + +# app + def _get_container_image(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None: + if self.wrapper_data.is_support_custom_container_image_for_app(app): + server = blueDeployment['properties']['source'].get('customContainer').get('server', '') + containerImage = blueDeployment['properties']['source'].get('customContainer').get('containerImage', '') + return f"{server}/{containerImage}" + else: + return None + +# module name + def _get_app_module_name(self, app): + appName = self._get_resource_name(app) + return appName.replace("-", "_") + + def _get_cert_module_name(self, cert): + certName = self._get_resource_name(cert) + return "cert_" + certName.replace("-", "_") + +# param name + # get param name of paramContainerAppImageName + def _get_param_name_of_container_image(self, app): + appName = self._get_resource_name(app) + return "containerImageOf_" + appName.replace("-", "_") + + # get param name of paramTargetPort + def _get_param_name_of_target_port(self, app): + appName = self._get_resource_name(app) + return "targetPortOf_" + appName.replace("-", "_") + + # get param name of paramContainerAppEnvStorageAccountKey + def _get_param_name_of_storage_account_key(self, disk_props): + storage_unique_name = self._get_storage_unique_name(disk_props) + return "containerAppEnvStorageAccountKeyOf_" + storage_unique_name + + # get param name of paramContainerAppImagePassword + def _get_param_name_of_container_image_password(self, app): + appName = self._get_resource_name(app) + return "containerImagePasswordOf_" + appName.replace("-", "_") + +class SourceDataWrapper: + def __init__(self, source): + self.source = source + + def get_resources_by_type(self, resource_type): + return [resource for resource in self.source['resources'] if resource['type'] == resource_type] + + def is_support_feature(self, feature): + return any(resource['type'] == feature for resource in self.source['resources']) + + def is_support_configserver(self): + return self.is_support_ossconfigserver() or self.is_support_acs() + + def is_support_configserver_for_app(self, app): + return self.is_support_ossconfigserver_for_app(app) or self.is_support_acs_for_app(app) + + def is_support_ossconfigserver(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/configServers') + + def is_support_ossconfigserver_for_app(self, app): + addon = app['properties'].get('addonConfigs') + if addon is None: + return False + return addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None + + def is_support_acs(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/configurationServices') + + def is_support_acs_for_app(self, app): + addon = app['properties'].get('addonConfigs') + if addon is None: + return False + return addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None + + def is_support_eureka(self): + return self.is_support_serviceregistry() or not self.is_enterprise_tier() + + def is_support_serviceregistry(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/serviceRegistries') + + def is_support_serviceregistry_for_app(self, app): + addon = app['properties'].get('addonConfigs') + if addon is None: + return False + return addon.get('serviceRegistry') is not None and addon['serviceRegistry'].get('resourceId') is not None + + def is_support_sba(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/applicationLiveViews') + + def is_support_gateway(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/gateways') + + def get_asa_service(self): + return self.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + + def get_apps(self): + return self.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + + def get_deployments(self): + return self.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') + + def get_deployments_by_app(self, app): + deployments = self.get_deployments() + return [deployment for deployment in deployments if deployment['name'].startswith(f"{app['name']}/")] + + def get_blue_deployment_by_app(self, app): + deployments = self.get_deployments_by_app(app) + deployments = [deployment for deployment in deployments if deployment['properties']['active'] is True] + return deployments[0] if deployments else None + + def get_green_deployment_by_app(self, app): + deployments = self.get_deployments_by_app(app) + deployments = [deployment for deployment in deployments if deployment['properties']['active'] is False] + return deployments[0] if deployments else None + + def get_green_deployments(self): + deployments = self.get_deployments() + deployments = [deployment for deployment in deployments if deployment['properties']['active'] is False] + return deployments if deployments else [] + + def get_build_results_deployments(self): + deployments = [] + deployments = self.get_deployments() + deployments = [deployment for deployment in deployments if deployment['properties'].get('source', {}).get('type', {}) == "BuildResult"] + return deployments + + def get_container_deployments(self): + deployments = [] + deployments = self.get_deployments() + deployments = [deployment for deployment in deployments if deployment['properties'].get('source', {}).get('type', {}) == "Container"] + return deployments + + def is_support_blue_green_deployment(self, app): + return len(self.get_deployments_by_app(app)) > 1 + + def get_custom_domains(self): + return self.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/domains') + + def get_custom_domains_by_app(self, app): + domains = self.get_custom_domains(self) + return [domain for domain in domains if domain['name'].startswith(f"{app['name']}/")] + + def is_enterprise_tier(self): + return self.get_asa_service()['sku']['tier'] == 'Enterprise' + + def is_vnet(self): + networkProfile = self.get_asa_service()['properties'].get('networkProfile') + if networkProfile is None: + return False + return networkProfile.get('appSubnetId') is not None + + def get_certificates(self): + return self.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') + + def get_keyvault_certificates(self): + return self.get_certificates_by_type("KeyVaultCertificate") + + def get_content_certificates(self): + return self.get_certificates_by_type("ContentCertificate") + + def get_certificates_by_type(self, type): + certs = [] + for cert in self.get_certificates(): + if cert['properties'].get('type') == type: + certs.append(cert) + return certs + + def get_storages(self): + return self.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + + def is_enabled_system_assigned_identity_for_app(self, app): + identity = app.get('identity') + if identity is None: + return False + return identity.get('type') == 'SystemAssigned' + + def is_support_custom_container_image_for_deployment(self, deployment): + if deployment is None: + return False + return deployment['properties'].get('source') is not None and \ + deployment['properties']['source'].get('customContainer') is not None and \ + deployment['properties']['source'].get('type') == 'Container' and \ + deployment['properties']['source']['customContainer'].get('containerImage') is not None + + def is_support_custom_container_image_for_app(self, app): + blueDeployment = self.get_blue_deployment_by_app(app) + if blueDeployment is None: + return False + return self.is_support_custom_container_image_for_deployment(blueDeployment) + + def is_private_custom_container_image(self, app): + blueDeployment = self.get_blue_deployment_by_app(app) + if blueDeployment is None: + return False + if self.is_support_custom_container_image_for_app(app): + return blueDeployment['properties']['source'].get('customContainer').get('imageRegistryCredential', {}).get('username', None) is not None diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py new file mode 100644 index 00000000000..d9c7fec53be --- /dev/null +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -0,0 +1,50 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.log import get_logger +from .base_converter import BaseConverter + +logger = get_logger(__name__) + + +# Concrete Converter Subclass for certificate +class CertConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + asa_content_certs = self.wrapper_data.get_content_certificates() + for cert in asa_content_certs: + logger.warning(f"Action Needed: The content certificate '{cert['name']}' cannot be exported automatically. Please export it manually.") + return self.wrapper_data.get_keyvault_certificates() + super().__init__(source, transform_data) + + def transform_data_item(self, cert): + isKeyVaultCert = False + cert_data = { + "certName": self._get_resource_name(cert), + "moduleName": self._get_cert_module_name(cert), + "certificateType": "ServerSSLCertificate", + } + certKeyVault = self._get_cert_key_vault(cert) + if certKeyVault: + cert_data["certificateKeyVaultProperties"] = certKeyVault + isKeyVaultCert = True + else: + cert_data["value"] = "*" + isKeyVaultCert = False + cert_data["isKeyVaultCert"] = isKeyVaultCert + return cert_data + + def get_template_name(self): + return "cert.bicep" + + def _get_cert_key_vault(self, cert): + certKeyVault = None + if cert['properties'].get('type') == "KeyVaultCertificate": + if cert['properties'].get('vaultUri') and cert['properties'].get('keyVaultCertName'): + certKeyVault = { + "keyVaultUrl": cert['properties']['vaultUri'] + "/secrets/" + cert['properties']['keyVaultCertName'], + "identity": "system" + } + return certKeyVault diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py new file mode 100644 index 00000000000..c5fef61545d --- /dev/null +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter +from knack.log import get_logger + +logger = get_logger(__name__) + + +# Concrete Converter Subclass for Config Server +class ConfigServerConverter(BaseConverter): + + CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" + KEY_URI = ".uri" + KEY_LABEL = ".default-label" + KEY_SEARCH_PATHS = ".search-paths" + KEY_USERNAME = ".username" + KEY_PASSWORD = ".password" + KEY_PRIVATE_KEY = ".private-key" + KEY_HOST_KEY = ".host-key" + KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm" + KEY_PATTERN = ".pattern" + + def __init__(self, source): + def transform_data(): + if self.wrapper_data.is_support_ossconfigserver(): + configServer = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers')[0] + name = "config" + configurations, params = self._get_configurations_and_params(configServer) + replicas = 2 + return { + "configServerName": name, + "params": params, + "configurations": configurations, + "replicas": replicas + } + else: + return None + super().__init__(source, transform_data) + + def get_template_name(self): + return "config_server.bicep" + + def _get_configurations_and_params(self, configServer): + configurations = [] + params = [] + + git_property = configServer.get('properties', {}).get('configServer', {}).get('gitProperty') + if git_property is not None: + self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, git_property.get('uri')) + self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, git_property.get('label')) + self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, git_property.get('searchPaths')) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, git_property.get('username'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, git_property.get('password'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, git_property.get('privateKey'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, git_property.get('hostKey'), configurations, params) + self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, git_property.get('hostKeyAlgorithm'), configurations, params) + + git_repos = git_property.get('repositories', []) + for repo in git_repos: + configuration_key_repo_prefix = self.CONFIGURATION_KEY_PREFIX + ".repos." + repo['name'] + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey'), configurations, params) + self._add_secret_config(configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm'), configurations, params) + self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_PATTERN, repo.get('pattern')) + + return configurations, params + + def _add_property_if_exists(self, configurations, key, value): + if value: + if isinstance(value, (list, tuple)): + value = ",".join(map(str, value)) + configurations.append({ + "propertyName": key, + "value": value + }) + + def _add_secret_config(self, key, value, configurations, params): + if value: + param_name = key.replace(".", "_").replace("-", "_") + self._add_property_if_exists(configurations, key, param_name) + params.append(param_name) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py new file mode 100644 index 00000000000..27bf23c7fb3 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import os + +from knack.log import get_logger + +logger = get_logger(__name__) + + +# Context Class +class ConversionContext: + def __init__(self, source): + self.source = source + self.converters = [] + + def register_converter(self, converter_class): + self.converters.append(converter_class) + + def run_converters(self): + converted_contents = {} + for converter_class in self.converters: + converter = converter_class(self.source) + items = converter.convert() + converted_contents.update(items) + return converted_contents + + def save_to_files(self, converted_contents, output_path): + logger.debug(f"Start to save the converted content to files in folder {os.path.abspath(output_path)}...") + os.makedirs(os.path.abspath(output_path), exist_ok=True) + + for filename, content in converted_contents.items(): + output_filename = os.path.join(output_path, filename) + with open(output_filename, 'w', encoding='utf-8') as output_file: + logger.info(f"Generating the file {output_filename}...") + output_file.write(content) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py new file mode 100644 index 00000000000..5ef811f3d3d --- /dev/null +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.log import get_logger +from .base_converter import BaseConverter + +logger = get_logger(__name__) + + +# Concrete Subclass for Container App Environment +class EnvironmentConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + asa_service = self.wrapper_data.get_asa_service() + name = self._get_resource_name(asa_service) + certs = self.wrapper_data.get_keyvault_certificates() + data = { + "containerAppEnvName": name, + "containerAppLogAnalyticsName": f"log-{name}", + "storages": self._get_app_storage_configs(), + } + if self._need_identity(certs): + data["identity"] = { + "type": "SystemAssigned", + } + if self.wrapper_data.is_vnet(): + data["vnetConfiguration"] = { + "internal": str(True).lower(), + } + + asa_zone_redundant = asa_service['properties'].get('zoneRedundant', False) + if asa_zone_redundant is not None: + if asa_zone_redundant is True and self.wrapper_data.is_vnet() is False: + logger.warning("Mismatch: Zone redundant is only supported in VNet environment for Azure Container Apps.") + data["zoneRedundant"] = str(False).lower() + else: + data["zoneRedundant"] = str(asa_zone_redundant).lower() + + asa_maintenance_window = asa_service['properties'].get('maintenanceScheduleConfiguration', None) + if asa_maintenance_window: + aca_maintenance_window = [{ + "weekDay": asa_maintenance_window['day'], + "startHourUtc": asa_maintenance_window['hour'], + "durationHours": 8, + }] + data["scheduledEntries"] = aca_maintenance_window + return data + super().__init__(source, transform_data) + + def get_template_name(self): + return "environment.bicep" + + def _need_identity(self, certs): + if certs is not None and len(certs) > 0: + return True + return False diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py new file mode 100644 index 00000000000..3744d9092cd --- /dev/null +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter + + +class EurekaConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + if self.wrapper_data.is_enterprise_tier() is False: + name = "eureka" + configurations = [] + replicas = 1 + + return { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + else: + return None + super().__init__(source, transform_data) + + def get_template_name(self): + return "eureka.bicep" diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py new file mode 100644 index 00000000000..38f9acbb95f --- /dev/null +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -0,0 +1,119 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.log import get_logger +from .base_converter import BaseConverter + +logger = get_logger(__name__) + + +class GatewayConverter(BaseConverter): + DEFAULT_NAME = "default" + + def __init__(self, source, client, resource_group, service): + def transform_data(): + if self.wrapper_data.is_support_gateway(): + gateway = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways')[0] + routes = [] + for gateway_route in self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways/routeConfigs'): + routes.append(gateway_route) + secretEnvs = self.client.gateways.list_env_secrets(self.resource_group, self.service, self.DEFAULT_NAME) + configurations = self._get_configurations(gateway, secretEnvs) + replicas = 2 + if gateway.get('sku', {}).get('capacity') is not None: + replicas = min(2, gateway['sku']['capacity']) + routes = self._get_routes(routes) + self._check_features(gateway.get('properties', {})) + self._check_custom_domains() + return { + "routes": routes, + "gatewayName": "gateway", + "configurations": configurations, + "replicas": replicas, + } + else: + return None + self.client = client + self.resource_group = resource_group + self.service = service + super().__init__(source, transform_data) + + def _get_configurations(self, gateway, secretEnvs): + configurations = [] + if gateway.get('properties', {}).get('environmentVariables', {}).get('properties') is not None: + for key, value in gateway['properties']['environmentVariables']['properties'].items(): + if key.startswith("spring.cloud.gateway") or key.startswith("logging"): + configurations.append({ + "propertyName": key, + "value": value, + }) + else: + logger.warning(f"Mismatch: The environment variable '{key}' is not supported in gateway for Spring in Azure Container Apps, see allowed configuration list of Gateway for Spring.") + if secretEnvs is not None: + for key, value in secretEnvs.items(): + configurations.append({ + "propertyName": key, + "value": value, + }) + return configurations + + def _get_routes(self, routes): + aca_routes = [] + name_counter = {} + if routes: + for route in routes: + base_name = self._get_resource_name(route) + aca_uri = self._get_uri_from_route(route) + if route.get('properties', {}).get('routes') is not None: + for r in route['properties']['routes']: + count = name_counter.get(base_name, 0) + 1 + name_counter[base_name] = count + aca_routes.append({ + "id": f"{base_name}_{count}", + "uri": r.get('uri', aca_uri), + "predicates": r.get('predicates') if r.get('predicates') else [], + "filters": self._get_filters(base_name, r), + "order": r.get('order') or 0, + }) + return aca_routes + + def _get_uri_from_route(self, route): + app_resource_id = route.get('properties', {}).get('appResourceId') + if app_resource_id: + app_name = self._get_app_name_from_app_resource_id(app_resource_id) + return f"http://{app_name}" + return + + def _get_app_name_from_app_resource_id(self, app_resource_id): + start = app_resource_id.rfind("'") + previous_comma = app_resource_id.rfind(",", 0, start) + return app_resource_id[previous_comma + 3:start] + + def _get_filters(self, route_name, r): + filters = [] + if r.get('filters'): + for f in r.get('filters'): + if 'cors' in f.lower(): + logger.warning(f"Action Needed: The cors filter '{f}' of route '{route_name}' is not supported in Gateway for Spring in Azure Container Apps, refer to migration doc for further steps.") + else: + filters.append(f) + return filters + + def _check_features(self, scg_properties): + if scg_properties.get('ssoProperties') is not None: + logger.warning("Mismatch: The SSO feature is not supported of Gateway for Spring in Azure Container Apps.") + if scg_properties.get('corsProperties') is not None and scg_properties.get('corsProperties') != {}: + logger.warning("Action Needed: CORS configuration detected, please refer to public doc to migrate CORS feature of Gateway for Spring to Azure Container Apps.") + if (scg_properties.get('apiMetadataProperties') is not None and scg_properties.get('apiMetadataProperties') != {}): + logger.warning("Mismatch: API metadata configuration is not supported of Gateway for Spring to Azure Container Apps.") + if (scg_properties.get('apmTypes') is not None and len(scg_properties.get('apmTypes')) > 0) or (scg_properties.get('apms') is not None and scg_properties.get('apms') != []): + logger.warning("Mismatch: APM configuration is not supported of Gateway for Spring to Azure Container Apps.") + + def _check_custom_domains(self): + custom_domains = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways/domains') + if custom_domains is not None and len(custom_domains) > 0: + logger.warning("Mismatch: Custom domains of gateway is not supported in Gateway for Spring of Azure Container Apps.") + + def get_template_name(self): + return "gateway.bicep" diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py new file mode 100644 index 00000000000..0a83cd20457 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter + + +class LiveViewConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + if self.wrapper_data.is_support_sba(): + # live_view = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews')[0] + name = "admin" + configurations = [] + replicas = 1 + return { + "sbaName": name, + "configurations": configurations, + "replicas": replicas + } + else: + return None + super().__init__(source, transform_data) + + def get_template_name(self): + return "spring_boot_admin.bicep" diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py new file mode 100644 index 00000000000..5e22ca006f1 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -0,0 +1,71 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter + + +# Concrete Converter Subclass for main +class MainConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + asa_certs = self.wrapper_data.get_keyvault_certificates() + certs = [] + for cert in asa_certs: + certName = self._get_resource_name(cert) + templateName = f"{certName}_cert.bicep" + certData = { + "certName": certName, + "moduleName": self._get_cert_module_name(cert), + "templateName": templateName, + } + certs.append(certData) + apps_data = [] + apps = self.wrapper_data.get_apps() + for app in apps: + appName = self._get_resource_name(app) + templateName = f"{appName}_app.bicep" + appData = { + "appName": appName, + "moduleName": self._get_app_module_name(app), + "templateName": templateName, + "paramContainerAppImageName": self._get_param_name_of_container_image(app), + "paramTargetPort": self._get_param_name_of_target_port(app), + "dependsOns": self._get_depends_on_list(app), + "isByoc": self.wrapper_data.is_support_custom_container_image_for_app(app), + "isPrivateImage": self.wrapper_data.is_private_custom_container_image(app), + "paramContainerAppImagePassword": self._get_param_name_of_container_image_password(app), + } + apps_data.append(appData) + + return { + "isVnet": self.wrapper_data.is_vnet(), + "certs": certs, + "apps": apps_data, + "storages": self._get_app_storage_configs(), + "gateway": self.wrapper_data.is_support_gateway(), + "config": self.wrapper_data.is_support_configserver(), + "eureka": self.wrapper_data.is_support_eureka(), + "sba": self.wrapper_data.is_support_sba(), + } + super().__init__(source, transform_data) + + def get_template_name(self): + return "main.bicep" + + def _get_depends_on_list(self, app): + service_bind = [] + if self.wrapper_data.is_support_configserver_for_app(app): + service_bind.append("managedConfig") + if self.wrapper_data.is_enterprise_tier() is not True and self.wrapper_data.is_support_ossconfigserver(): + # standard tier enabled config server and bind all apps automatically + service_bind.append("managedConfig") + if self.wrapper_data.is_support_serviceregistry_for_app(app): + service_bind.append("managedEureka") + if self.wrapper_data.is_enterprise_tier() is not True and self.wrapper_data.is_support_eureka(): + # standard tier enabled eureka server and bind all apps automatically + service_bind.append("managedEureka") + if self.wrapper_data.is_support_sba(): + service_bind.append("managedSpringBootAdmin") + return service_bind diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py new file mode 100644 index 00000000000..85d5ec6f41a --- /dev/null +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -0,0 +1,34 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter + + +# Concrete Converter Subclass for paramter +class ParamConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + apps = self.wrapper_data.get_apps() + apps_data = [] + for app in apps: + apps_data.append({ + "appName": self._get_resource_name(app), + "paramContainerAppImageName": self._get_param_name_of_container_image(app), + "paramTargetPort": self._get_param_name_of_target_port(app), + "isByoc": self.wrapper_data.is_support_custom_container_image_for_app(app), + "isPrivateImage": self.wrapper_data.is_private_custom_container_image(app), + "paramContainerAppImagePassword": self._get_param_name_of_container_image_password(app), + "image": self._get_container_image(app), + }) + + return { + "apps": apps_data, + "storages": self._get_app_storage_configs(), + "isVnet": self.wrapper_data.is_vnet() + } + super().__init__(source, transform_data) + + def get_template_name(self): + return "param.bicepparam" diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py new file mode 100644 index 00000000000..6bc85124c2d --- /dev/null +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -0,0 +1,68 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter + + +# Concrete Converter Subclass for Read Me +class ReadMeConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + custom_domains = self.wrapper_data.get_custom_domains() + apps = self.wrapper_data.get_apps() + keyvault_certs = self.wrapper_data.get_keyvault_certificates() + content_certs = self.wrapper_data.get_content_certificates() + green_deployments = self.wrapper_data.get_green_deployments() + should_system_assigned_identity_enabled = len(self.wrapper_data.get_keyvault_certificates()) > 0 + + data = { + "isVnet": self.wrapper_data.is_vnet(), + "containerDeployments": self.wrapper_data.get_container_deployments(), + "buildResultsDeployments": self.wrapper_data.get_build_results_deployments(), + "hasApps": len(apps) > 0, + "isSupportGateway": self.wrapper_data.is_support_gateway(), + "isSupportConfigServer": self.wrapper_data.is_support_configserver(), + "customDomains": self._transform_domains(custom_domains), + "hasCerts": len(keyvault_certs) > 0 or len(content_certs) > 0, + "keyVaultCerts": keyvault_certs, + "contentCerts": content_certs, + "greenDeployments": self._transform_deployments(green_deployments), + "shouldSystemAssignedIdentityEnabled": should_system_assigned_identity_enabled, + "systemAssignedIdentityApps": self._get_system_assigned_identity_apps(), + } + # print(f"ReadMeConverter data: {data}") + return data + super().__init__(source, transform_data) + + def get_template_name(self): + return "README.md" + + def _transform_deployments(self, deployments): + deployments_data = [] + for deployment in deployments: + deployment_data = { + "appName": self._get_parent_resource_name(deployment), + "name": self._get_resource_name(deployment), + } + deployments_data.append(deployment_data) + return deployments_data + + def _transform_domains(self, domains): + domains_data = [] + for domain in domains: + domain_data = { + "appName": self._get_parent_resource_name(domain), + "name": self._get_resource_name(domain), + } + domains_data.append(domain_data) + return domains_data + + def _get_system_assigned_identity_apps(self): + apps = self.wrapper_data.get_apps() + system_assigned_identity_apps = [] + for app in apps: + if self.wrapper_data.is_enabled_system_assigned_identity_for_app(app): + system_assigned_identity_apps.append(app) + return system_assigned_identity_apps diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py new file mode 100644 index 00000000000..f5a6faab5f6 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -0,0 +1,31 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base_converter import BaseConverter + + +class ServiceRegistryConverter(BaseConverter): + + def __init__(self, source): + def transform_data(): + if self.wrapper_data.is_enterprise_tier(): + if self.wrapper_data.is_support_serviceregistry(): + # service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') + name = "eureka" + configurations = [] + replicas = 1 + + return { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + else: + return None + else: + return None + super().__init__(source, transform_data) + + def get_template_name(self): + return "eureka.bicep" diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 new file mode 100644 index 00000000000..738dc850d0d --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -0,0 +1,266 @@ +param containerAppName string = '{{data.containerAppName}}' +{%- if data.isByoc %} +{%- if data.isPrivateImage %} +@secure() +param {{data.paramContainerAppImagePassword}} string +{%- endif %} +{%- else %} +param {{data.paramContainerAppImageName}} string +{%- endif %} +param {{data.paramTargetPort}} int + +@description('Minimum number of replicas that will be deployed') +@minValue(0) +param minReplicas int = {{data.minReplicas}} + +@description('Maximum number of replicas that will be deployed') +@minValue(1) +param maxReplicas int = {{data.maxReplicas}} + +param containerAppEnvId string +param workloadProfileName string + +resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { + name: containerAppName + location: resourceGroup().location + {%- if data.identity %} + identity: { + type: '{{data.identity.type}}' + {%- if data.identity.userAssignedIdentities %} + userAssignedIdentities: { + {%- for miResourceId in data.identity.userAssignedIdentities %} + '{{miResourceId}}': {} + {%- if not loop.last %}{%- endif %} + {%- endfor %} + } + {%- endif %} + } + {%- endif %} + properties: { + managedEnvironmentId: containerAppEnvId + workloadProfileName: workloadProfileName + configuration: { + {%- if data.isByoc %} + {%- if data.isPrivateImage %} + registries: [ + { + server: '{{data.containerRegistry.server}}' + username: '{{data.containerRegistry.username}}' + passwordSecretRef: '{{data.containerRegistry.passwordSecretRef}}' + } + ] + {%- endif %} + {%- if data.isPrivateImage %} + secrets: [ + { + name: '{{data.containerRegistry.passwordSecretRef}}' + value: {{data.paramContainerAppImagePassword}} + } + ] + {%- endif %} + {%- endif %} + ingress: { + {%- if data.ingress %} + external: {% if data.isPublic %}true{% else %}false{% endif %} + targetPort: {{data.paramTargetPort}} + allowInsecure: false + transport: '{{data.ingress.transport}}' + {%- if data.ingress.sessionAffinity %} + stickySessions: { + affinity: '{{data.ingress.sessionAffinity}}' + } + {%- endif %} + clientCertificateMode: 'Ignore' + traffic: [ + { + latestRevision: true + weight: 100 + } + ] + {%- endif %} + } + activeRevisionsMode: '{% if data.isBlueGreen %}Multiple{% else %}Single{% endif %}' + } + template: { + // revisionSuffix: '{{ data.blue.name }}' + containers: [ + { + name: containerAppName + {%- if data.isByoc %} + image: '{{data.containerRegistry.image}}' + {%- else %} + image: {{data.paramContainerAppImageName}} + {%- endif %} + {%- if data.blue %} + resources: { + cpu: json('{{data.blue.cpuCore}}') + memory: '{{data.blue.memorySize}}' + } + {%- endif %} + {%- if data.args %} + args: [ + {%- for arg in data.args %} + '{{arg}}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + {%- if data.commands %} + command: [ + {%- for command in data.commands %} + '{{command}}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + {%- if data.blue %} + env: [ + {%- for env in data.blue.env %} + { + name: '{{ env.name }}' + value: '{{ env.value }}' + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + {%- if data.blue %} + probes: [ + {%- if data.blue.livenessProbe %} + { + {%- if data.blue.livenessProbe.periodSeconds %} + periodSeconds: {{data.blue.livenessProbe.periodSeconds}} + {%- endif %} + {%- if data.blue.livenessProbe.httpGet %} + httpGet: { + path: '{{data.blue.livenessProbe.httpGet.path}}' + port: {{data.paramTargetPort}} + scheme: '{{data.blue.livenessProbe.httpGet.scheme}}' + } + {%- endif %} + {%- if data.blue.livenessProbe.tcpSocket %} + tcpSocket: { + port: {{data.paramTargetPort}} + } + {%- endif %} + {%- if data.blue.livenessProbe.initialDelaySeconds %} + initialDelaySeconds: {{data.blue.livenessProbe.initialDelaySeconds}} + {%- endif %} + {%- if data.blue.livenessProbe.timeoutSeconds %} + timeoutSeconds: {{data.blue.livenessProbe.timeoutSeconds}} + {%- endif %} + {%- if data.blue.livenessProbe.successThreshold %} + successThreshold: {{data.blue.livenessProbe.successThreshold}} + {%- endif %} + {%- if data.blue.livenessProbe.failureThreshold %} + failureThreshold: {{data.blue.livenessProbe.failureThreshold}} + {%- endif %} + type: 'Liveness' + } + {%- endif %} + {%- if data.blue.readinessProbe %} + { + {%- if data.blue.readinessProbe.periodSeconds %} + periodSeconds: {{data.blue.readinessProbe.periodSeconds}} + {%- endif %} + {%- if data.blue.readinessProbe.httpGet %} + httpGet: { + path: '{{data.blue.readinessProbe.httpGet.path}}' + port: {{data.paramTargetPort}} + scheme: '{{data.blue.readinessProbe.httpGet.scheme}}' + } + {%- endif %} + {%- if data.blue.readinessProbe.tcpSocket %} + tcpSocket: { + port: {{data.paramTargetPort}} + } + {%- endif %} + {%- if data.blue.readinessProbe.initialDelaySeconds %} + initialDelaySeconds: {{data.blue.readinessProbe.initialDelaySeconds}} + {%- endif %} + {%- if data.blue.readinessProbe.timeoutSeconds %} + timeoutSeconds: {{data.blue.readinessProbe.timeoutSeconds}} + {%- endif %} + {%- if data.blue.readinessProbe.successThreshold %} + successThreshold: {{data.blue.readinessProbe.successThreshold}} + {%- endif %} + {%- if data.blue.readinessProbe.failureThreshold %} + failureThreshold: {{data.blue.readinessProbe.failureThreshold}} + {%- endif %} + type: 'Readiness' + } + {%- endif %} + {%- if data.blue.startupProbe %} + { + {%- if data.blue.startupProbe.periodSeconds %} + periodSeconds: {{data.blue.startupProbe.periodSeconds}} + {%- endif %} + {%- if data.blue.startupProbe.httpGet %} + httpGet: { + path: '{{data.blue.startupProbe.httpGet.path}}' + port: {{data.paramTargetPort}} + scheme: '{{data.blue.startupProbe.httpGet.scheme}}' + } + {%- endif %} + {%- if data.blue.startupProbe.tcpSocket %} + tcpSocket: { + port: {{data.paramTargetPort}} + } + {%- endif %} + {%- if data.blue.startupProbe.initialDelaySeconds %} + initialDelaySeconds: {{data.blue.startupProbe.initialDelaySeconds}} + {%- endif %} + {%- if data.blue.startupProbe.timeoutSeconds %} + timeoutSeconds: {{data.blue.startupProbe.timeoutSeconds}} + {%- endif %} + {%- if data.blue.startupProbe.successThreshold %} + successThreshold: {{data.blue.startupProbe.successThreshold}} + {%- endif %} + {%- if data.blue.startupProbe.failureThreshold %} + failureThreshold: {{data.blue.startupProbe.failureThreshold}} + {%- endif %} + type: 'Startup' + } + {%- endif %} + ] + {%- endif %} + {%- if data.volumeMounts %} + volumeMounts: [ + {%- for volume in data.volumeMounts %} + { + volumeName: '{{ volume.volumeName }}' + mountPath: '{{ volume.mountPath }}' + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + } + ] + {%- if data.volumes %} + volumes: [ + {%- for volume in data.volumes %} + { + name: '{{ volume.volumeName }}' + storageName: '{{ volume.storageName }}' + mountOptions: '{{ volume.mountOptions }}' + storageType: 'AzureFile' + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + serviceBinds: [ + {%- for bind in data.serviceBinds %} + { + name: '{{ bind.name }}' + serviceId: {{ bind.serviceId }} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + scale: { + minReplicas: minReplicas + maxReplicas: maxReplicas + } + } + } +} + +output containerAppFQDN string = {{data.moduleName}}.properties.configuration.ingress.fqdn diff --git a/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 new file mode 100644 index 00000000000..36f2255cfcf --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 @@ -0,0 +1,19 @@ +param managedEnvironments_aca_env_name string + +resource {{data.moduleName}} 'Microsoft.App/managedEnvironments/certificates@2024-10-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.certName }}' + location: resourceGroup().location + properties: { + certificateType: '{{ data.certificateType }}' + {%- if data.certificateKeyVaultProperties %} + certificateKeyVaultProperties: { + keyVaultUrl: '{{ data.certificateKeyVaultProperties.keyVaultUrl }}' + identity: '{{ data.certificateKeyVaultProperties.identity }}' + } + {%- endif %} + {%- if data.value %} + value: '{{ data.value }}' + password: '' + {%- endif %} + } +} diff --git a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 new file mode 100644 index 00000000000..4dd4e3d267e --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -0,0 +1,28 @@ +param managedEnvironments_aca_env_name string +{%- if data.params | length > 0 %} +// Provide the credentials in the parameters below to access the Git repositories. +// Example: param spring_cloud_config_server_git_username string = 'username' +{%- for param in data.params %} +@secure() +param {{param}} string = ' // please fill in the value +{%- endfor %} + +{%- endif %} +resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.configServerName }}' + properties: { + componentType: 'SpringCloudConfig' + configurations: [ + {%- for config in data.configurations %} + { + propertyName: '{{ config.propertyName }}' + value: {% if config.value in data.params %}{{ config.value }}{% else %}'{{ config.value }}'{% endif %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + scale: { + minReplicas: {{ data.replicas }} + maxReplicas: {{ data.replicas }} + } + } +} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 new file mode 100644 index 00000000000..f2ae7479a3a --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -0,0 +1,99 @@ +// Params +param workloadProfileName string +param workloadProfileType string +param minNodes int +param maxNodes int +{%- if data.vnetConfiguration %} +param vnetSubnetId string +{%- endif %} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: '{{data.containerAppLogAnalyticsName}}' + location: resourceGroup().location + properties: { + sku: { + name: 'PerGB2018' + } + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + scope: containerAppEnv + name: 'default' + properties: { + workspaceId: logAnalytics.id + logs: [ + { + categoryGroup: 'AllLogs' + enabled: true + } + ] + } +} + +resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { + name: '{{data.containerAppEnvName}}' + location: resourceGroup().location + {%- if data.identity %} + identity: { + type: '{{data.identity.type}}' + } + {%- endif %} + properties: { + {%- if data.vnetConfiguration %} + vnetConfiguration: { + internal: {{ data.vnetConfiguration.internal }} + infrastructureSubnetId: vnetSubnetId + } + {%- endif %} + appLogsConfiguration: { + destination: 'azure-monitor' + } + zoneRedundant: {{data.zoneRedundant}} + workloadProfiles: [ + { + name: workloadProfileName + workloadProfileType: workloadProfileType + minimumCount: minNodes + maximumCount: maxNodes + } + ] + } +} + +{%- if data.scheduledEntries %} +resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigurations@2024-10-02-Preview' = { + name: 'default' + parent: containerAppEnv + location: resourceGroup().location + properties: { + scheduledEntries: [ + {%- for entry in data.scheduledEntries %} + { weekDay: '{{ entry.weekDay }}', startHourUtc: {{ entry.startHourUtc }}, durationHours: {{ entry.durationHours }} }{{ "," if not loop.last }} + {%- endfor %} + ] + } +} +{%- endif %} + +{%- if data.storages %} +{%- for storage in data.storages %} +@secure() +param {{storage.paramContainerAppEnvStorageAccountKey}} string +resource {{storage.storageName}} 'Microsoft.App/managedEnvironments/storages@2024-08-02-preview' = { + parent: containerAppEnv + name: '{{storage.storageName}}' + properties: { + azureFile: { + accountName: '{{storage.accountName}}' + shareName: '{{storage.shareName}}' + accessMode: '{{storage.accessMode}}' + accountKey: {{storage.paramContainerAppEnvStorageAccountKey}} + } + } +} +{%- endfor %} +{%- endif %} + +output containerAppEnvId string = containerAppEnv.id +output containerAppEnvName string = containerAppEnv.name diff --git a/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 new file mode 100644 index 00000000000..b1e25593502 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 @@ -0,0 +1,20 @@ +param managedEnvironments_aca_env_name string + +resource eurekaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.eurekaName }}' + properties: { + componentType: 'SpringCloudEureka' + configurations: [ + {%- for config in data.configurations %} + { + propertyName: '{{ config.propertyName }}' + value: '{{ config.value }}' + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + scale: { + minReplicas: {{ data.replicas }} + maxReplicas: {{ data.replicas }} + } + } +} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 new file mode 100644 index 00000000000..bd1c067b344 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 @@ -0,0 +1,41 @@ +param managedEnvironments_aca_env_name string + +resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.gatewayName }}' + properties: { + componentType: 'SpringCloudGateway' + configurations: [ + {%- for config in data.configurations %} + { + propertyName: '{{ config.propertyName }}' + value: '{{ config.value }}' + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + scale: { + minReplicas: {{ data.replicas }} + maxReplicas: {{ data.replicas }} + } + springCloudGatewayRoutes: [ + {%- for route in data.routes %} + { + id: '{{ route.id }}' + uri: '{{ route.uri }}' + order: {{ route.order }} + predicates: [ + {%- for predicate in route.predicates %} + '{{ predicate }}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + filters: [ + {%- for filter in route.filters %} + '{{ filter }}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + } +} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 new file mode 100644 index 00000000000..5791e57e602 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -0,0 +1,110 @@ +// Params +param workloadProfileName string +param workloadProfileType string +param minNodes int +param maxNodes int + +{%- if data.isVnet == true %} +param vnetSubnetId string +{%- endif %} +{%- for app in data.apps %} +{%- if app.isByoc %} +{%- if app.isPrivateImage %} +@secure() +param {{app.paramContainerAppImagePassword}} string +{%- endif %} +{%- else %} +param {{app.paramContainerAppImageName}} string +{%- endif %} +param {{app.paramTargetPort}} int +{%- endfor %} +{%- for storage in data.storages %} +@secure() +param {{storage.paramContainerAppEnvStorageAccountKey}} string +{%- endfor %} + +module containerAppEnv 'environment.bicep' = { + name: 'container-app-environment-Deployment' + params: { + workloadProfileName: workloadProfileName + workloadProfileType: workloadProfileType + minNodes: minNodes + maxNodes: maxNodes + {%- if data.isVnet == true %} + vnetSubnetId: vnetSubnetId + {%- endif %} + {%- for storage in data.storages %} + {{storage.paramContainerAppEnvStorageAccountKey}}: {{storage.paramContainerAppEnvStorageAccountKey}} + {%- endfor %} + } +} + +{%- for cert in data.certs %} +module {{ cert.moduleName }} '{{ cert.templateName }}' = { + name: 'cert-{{ cert.certName }}-Deployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} +{%- endfor %} + +{%- for app in data.apps %} +module {{ app.moduleName }} '{{ app.templateName }}' = { + name: '{{ app.appName }}-Deployment' + {%- if app.dependsOns %} + dependsOn: [ + {%- for dependsOn in app.dependsOns %} + {{ dependsOn }} + {%- endfor %} + ] + {%- endif %} + params: { + containerAppEnvId: containerAppEnv.outputs.containerAppEnvId + workloadProfileName: workloadProfileName + {%- if app.isByoc %} + {%- if app.isPrivateImage %} + {{app.paramContainerAppImagePassword}}: {{app.paramContainerAppImagePassword}} + {%- endif %} + {%- else %} + {{app.paramContainerAppImageName}}: {{app.paramContainerAppImageName}} + {%- endif %} + {{app.paramTargetPort}}: {{app.paramTargetPort}} + } +} +{%- endfor %} + +{%- if data.gateway == true %} +module managedGateway 'gateway.bicep' = { + name: 'gateway-Deployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} +{%- endif %} + +{%- if data.config == true %} +module managedConfig 'config_server.bicep' = { + name: 'config-server-Deployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} +{%- endif %} + +{%- if data.eureka == true %} +module managedEureka 'eureka.bicep' = { + name: 'eureka-Deployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} +{%- endif %} + +{%- if data.sba == true %} +module managedSpringBootAdmin 'spring_boot_admin.bicep' = { + name: 'spring-boot-admin-Deployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} +{%- endif %} diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 new file mode 100644 index 00000000000..d8d2dd5bd76 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -0,0 +1,30 @@ +using './main.bicep' + +param workloadProfileName = 'Dedicated' +param workloadProfileType = 'D4' +param minNodes = 1 +param maxNodes = 10 + +{%- if data.isVnet == true %} +// Provide the full resource ID of the existing subnet, the subnet must be delegated to Microsoft.App/environments +// Example: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} +param vnetSubnetId = +{%- endif %} + +{%- for app in data.apps %} +{%- if app.isByoc %} +param {{app.paramTargetPort}} = 8080 +{%- if app.isPrivateImage %} +@secure() +param {{app.paramContainerAppImagePassword}} = 'fill in password for container retistry "{{app.image}}"' +{%- endif %} +{%- else %} +param {{app.paramContainerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' +param {{app.paramTargetPort}} = 80 +{%- endif %} +{%- endfor %} + +{%- for storage in data.storages %} +@secure() +param {{storage.paramContainerAppEnvStorageAccountKey}} = 'fill in account key for storage "{{storage.accountName}}"' +{%- endfor %} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 new file mode 100644 index 00000000000..01fe7eb4b5b --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -0,0 +1,183 @@ +This README provides instructions on how to use the Bicep files generated from your Azure Spring Apps service to provision an Azure Container Apps environment, including the necessary resources and containerized applications. + +Due to limitations in the Azure Container Apps backends, multiple deployment rounds may be expected to successfully provision all the resources. + +After deploying the Bicep files, some configurations must be updated manually by following the steps outlined in [Azure public documentation](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-overview). + +## Prerequisites + +Before getting started, make sure you have the following: + +1. **Azure Subscription**: An Azure subscription for resources of Azure Container Apps. + +2. **Bicep CLI**: Verify that the Bicep CLI is installed. + +## Steps to Deploy Azure Container Apps Using the Bicep Files + +### Review the Bicep Files + +After generating the Bicep files, the specified output directory will contain: + +- `main.bicep`: The entry point to deploy and manage Azure Container Apps resources. +- `param.bicepparam`: The parameters which values required for the deployment. +- `environment.bicep`: The Bicep file that contains the resource definitions for the Azure Container Apps environment. +- `-app.bicep`: The Bicep file that contains the resource definitions for the Azure Container Apps applications. +- `.bicep`: The Bicep file that contains the resource definitions for the managed components like config server, spring cloud gateway, spring boot admin and eureka. + +These Bicep files enable quick deployment of the Azure Container Apps environment and applications. However you can also customize the parameters and configurations in the `param.bicepparam` file and Bicep files to suit your environment. + +### Customize the Bicep Files + +Ensure the parameter values in `param.bicepparam` are properly set to configure your Azure Container Apps. + +{%- if data.isVnet %} +#### VNet configuration + +To migrate a custom virtual network, you must [create the VNet](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-network#create-an-azure-container-apps-environment-with-a-virtual-network) and specify the subnet ID in the `param.bicepparam` file. +{%- endif %} + +{%- if data.hasApps %} +#### Application Containerization and Deployment + +{%- if data.buildResultsDeployments %} +For the following applications/deployments built by Azure Spring Apps, you need to build the image and deploy it to Azure Container Apps. +{%- for deployment in data.buildResultsDeployments %} + - `{{deployment.name}}` +{%- endfor %} +{%- endif %} +{{ '\n' }} +{%- if data.containerDeployments %} +For the following applications/deployments using custom container registry, you need to provide the container registry password (if required) and target port in `param.bicepparam`. +{%- for deployment in data.containerDeployments %} + - `{{deployment.name}}` +{%- endfor %} +{%- endif %} + +{%- endif %} + +You can choose one of the following options: +- **Option 1**: Leave the image URL in `param.bicepparam` unchanged to deploy the quickstart applications. You can deploy your own applications later, after the Azure Container Apps environment is created. +- **Option 2**: Replace the image URL with your application's image and update the corresponding target ports. For more details on obtaining container images, refer to [Application containerization](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-build-overview), and learn how to [deploy them](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#deploy-an-application). If your images are hosted in private repositories in Azure Container Registry, you will need to provide the necessary credentials to Azure Container Apps. For more information, refer to [this doc](https://learn.microsoft.com/azure/container-apps/managed-identity-image-pull). + + +#### Managed Components Configuration +{%- if data.isSupportGateway %} +- To migrate the Spring Cloud Gateway, follow [this doc](https://aka.ms/asa-scg-migration) to set up a self-hosted gateway in Azure Container Apps. Alternatively you can use the managed Gateway for Spring, which is currently in public preview. +{%- endif %} +{%- if data.isSupportConfigServer %} +- Update Git repository credentials in the `config_server.bicep` file if using private remote Git repositories, such as providing the credentials. +{%- endif %} + +You can refer to the migration doc on managed components for more details. + +#### Mismatch during migration + +During the script execution, two types of messages may appear: + +1. **Mismatch Message**: Sometimes, the properties in Azure Spring Apps and Azure Container Apps do not completely match. In such cases, a mismatch message will be displayed to inform the user. Here is a sample: + + ``` + "Mismatch Detected: Property 'X' in Azure Spring Apps does not match any Property in Azure Container Apps." + ``` + +2. **Action Message**: Occasionally, some post actions need to be taken by the user. In these instances, the system will display a warning message. Please refer to the post-deployment configuration for further details. Here is a sample: + + ``` + "Action Needed: This service uses blue-green deployment. The green deployment needs to be done manually." + ``` + +### Deploy the Bicep files using the Azure CLI: + +**Step 1**: If the resource group for your Azure Container Apps deployment doesn't already exist, create one using the following command: + +```azurecli +az group create --name --location --subscription +``` + +**Step 2**: Deploy the Bicep files using the following command: + +```azurecli +az deployment group create --resource-group --template-file main.bicep --parameters param.bicepparam --subscription +``` + +Since there are dependencies between Azure Container Apps components, you may need to deploy the Bicep files multiple times to complete the creation of all components. Additionally, errors may occur during deployments, and you can refer to **Step 3** for solutions. + +**Bicep Validation Failed Message**: If there are issues with the Bicep template validation, a Bicep validation failed message will be displayed. Here is a sample: + +``` +Error BCP028: Identifier "xxx" is declared multiple times. Remove or rename the duplicates. [https://aka.ms/bicep/core-diagnostics#BCP028] +``` + +Normally, this script does not take too long to complete. However, if you find that it has been stuck for an hour or more, please do not hesitate to cancel the deployment on the portal. There might be something wrong, and canceling the operation can help speed up the process. + +**Step 3**: Validate the deployment: + +After deployment, verify that all resources, including the Azure Container Apps environment and applications, are properly provisioned. + +You can check the deployment status either in the CLI output or in the Azure Portal: + +- Navigate to the Azure Portal and open the resource group you specified. +- Click `Settings` > `Deployments` to view all the deployment status and any errors. +- If there is any error, check the error messages and logs to troubleshoot the issue. Check if it meets the following known issues: + | Error Code | Error Message | Action | + |---|---|---| + | ManagedIdentityNoPermission | The Managed Identity 'system' does not have permission for resource... | This error typically occurs when migrating Key Vault certificates. Since the system-assigned MI for ACA is created along with the environment, you need to assign it the **Key Vault Secrets User** role in your key vault after the environment is created, then redeploy the Bicep files.​ | + | JavaComponentOperationError | Failed to create config map external-auth-config-map for JavaComponent '' in k8se-system namespace. | Creating a Java component immediately after the environment is set up sometimes fails due to a server-side issue. you can redeploy the Bicep files to solve this issue. | + | JavaComponentOperationError | Failed to provision java component '', Java Component is invalid, reason: admission webhook \"javacomponent.kb.io\" denied the request... | Fix the properties in the error message and redeploy the bicep files. Or find more details in the logs of managed components. | + | InvalidCertificateValueFormat | The certificate value should be base64 string. | Manually upload certificate to Azure Container Apps environment. | + | ManagedEnvironmentSubnetDelegationError | The subnet of the environment must be delegated to the service 'Microsoft.App/environments'. | Update subnet with param `--delegations Microsoft.App/environments` to delegate subnet | + +{{ '\n' }} + +## Post-Deployment Configuration +Some properties and configurations are not included in the Bicep files and must be manually updated. + +{%- if data.hasCerts %} +### **TLS certificate** +{%- if data.shouldSystemAssignedIdentityEnabled %} +Following TLS certificates from Azure Key Vault have been migrated by the migration tool, to make sure Azure Container Apps can load that certificate, you need to assign **Key Vault Secrets User** role to the system-assigned managed identity of Azure Container Apps environment after it creates. +{%- for cert in data.keyVaultCerts %} + - `{{cert.name}}` +{%- endfor %} +{%- endif %} + +{%- if data.contentCerts %} + +Please be aware that the following certificates cannot be migrated automatically due to API limitations. If necessary, you can manually upload the original certificate file to the Azure Container Apps environment: +{%- for cert in data.contentCerts %} + - `{{cert.name}}` +{%- endfor %} +{%- endif %} +{%- endif %} + +{%- if data.customDomains %} +### Custom Domain +You have custom domains configured in Azure Spring Apps. You can follow the steps in [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. Please manually update custom domains of following applications: +{%- for domain in data.customDomains %} + - `{{domain.appName}}/{{domain.name}}` +{%- endfor %} +{%- endif %} + +{%- if data.systemAssignedIdentityApps %} +### Managed Identity +Following applications have enabled system-assigned managed identity in Azure Spring Apps, your Azure Container Apps app instance will also have enabled system-assigned managed identity, but you need to reassign the corresponding roles, which you can identify from the error message if the deployment fails, to that managed identity. You can refer to [this doc](https://learn.microsoft.com/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. +{%- for app in data.systemAssignedIdentityApps %} + - `{{app.name}}` +{%- endfor %} +{%- endif %} + +{%- if data.greenDeployments %} +### Blue-green deployment +Following applications have enabled blue-green deployments in Azure Spring Apps, the **blue** deployments have been migrated to Azure Container Apps, but you need to follow the steps in [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. +{%- for deployment in data.greenDeployments %} + - Please manually deploy your staging deployment `{{deployment.appName}}/{{deployment.name}}`. Refer to this [doc](https://learn.microsoft.com/azure/spring-apps/basic-standard/how-to-staging-environment). +{%- endfor %} +{%- endif %} + +### Auto scale +Since Azure Container Apps leverage Kubernetes Event-driven Autoscaling (KEDA) for auto-scaling, which is different from Azure Spring Apps that uses Azure Monitor, the tool cannot transfer these settings for you. You need to manually configure the auto-scaling settings in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. + +### Monitoring +The migration has enabled logging and monitoring in Azure Container Apps. You can refer to [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. + +Refer to the [official migration documentation](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) for more details on feature migration. \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/spring_boot_admin.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/spring_boot_admin.bicep.j2 new file mode 100644 index 00000000000..95a50b403c3 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/spring_boot_admin.bicep.j2 @@ -0,0 +1,20 @@ +param managedEnvironments_aca_env_name string + +resource sbaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.sbaName }}' + properties: { + componentType: 'SpringBootAdmin' + configurations: [ + {%- for config in data.configurations %} + { + propertyName: '{{ config.propertyName }}' + value: '{{ config.value }}' + }{%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + scale: { + minReplicas: {{ data.replicas }} + maxReplicas: {{ data.replicas }} + } + } +} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py new file mode 100644 index 00000000000..5f1e70ef4d9 --- /dev/null +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -0,0 +1,77 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from azure.cli.command_modules.resource._client_factory import (_resource_client_factory) +from azure.cli.core.commands import LongRunningOperation +from azure.cli.core.commands.client_factory import get_subscription_id +from knack.log import get_logger +from .converter.conversion_context import ConversionContext +from .converter.environment_converter import EnvironmentConverter +from .converter.app_converter import AppConverter +from .converter.gateway_converter import GatewayConverter +from .converter.eureka_converter import EurekaConverter +from .converter.service_registry_converter import ServiceRegistryConverter +from .converter.config_server_converter import ConfigServerConverter +from .converter.acs_converter import ACSConverter +from .converter.live_view_converter import LiveViewConverter +from .converter.readme_converter import ReadMeConverter +from .converter.main_converter import MainConverter +from .converter.param_converter import ParamConverter +from .converter.cert_converter import CertConverter + +logger = get_logger(__name__) + + +def migration_aca_start(cmd, client, resource_group, service, output_folder): + logger.info("Getting your Azure Spring Apps service...") + logger.debug("Start to export ARM template for Azure Spring Apps service...") + asa_arm = export_asa_arm_template(cmd, resource_group, service) + + # Create context and add converters + context = ConversionContext(asa_arm) + context.register_converter(MainConverter) + context.register_converter(EnvironmentConverter) + context.register_converter(AppConverter) + context.register_converter(lambda param: GatewayConverter(param, client, resource_group, service)) + context.register_converter(EurekaConverter) + context.register_converter(ServiceRegistryConverter) + context.register_converter(ConfigServerConverter) + context.register_converter(ACSConverter) + context.register_converter(LiveViewConverter) + context.register_converter(ReadMeConverter) + context.register_converter(ParamConverter) + context.register_converter(CertConverter) + + # Run all converters + logger.warning("Converting resources to Azure Container Apps...") + converted_contents = context.run_converters() + + logger.debug("Start to save the converted content to files...") + # Save each line of converted content to a separate file + context.save_to_files(converted_contents, output_folder) + logger.warning(f"Successfully generated the Bicep files in folder '{output_folder}'. Please review the files and follow the instructions in the `README.md` for the next steps.") + + +def export_asa_arm_template(cmd, resource_group, service): + resources = [] + subscription = get_subscription_id(cmd.cli_ctx) + service_resource_id = '/subscriptions/{}/resourceGroups/{}/providers/Microsoft.AppPlatform/Spring/{}'.format( + subscription, resource_group, service) + logger.info("service_resource_id: '%s'", service_resource_id) + resources.append(service_resource_id) + options = "SkipAllParameterization,IncludeParameterDefaultValue" + + ExportTemplateRequest = cmd.get_models('ExportTemplateRequest') + export_template_request = ExportTemplateRequest(resources=resources, options=options) + + rcf = _resource_client_factory(cmd.cli_ctx) + + if cmd.supported_api_version(min_api='2019-08-01'): + result_poller = rcf.resource_groups.begin_export_template(resource_group, + parameters=export_template_request) + result = LongRunningOperation(cmd.cli_ctx)(result_poller) + else: + result = rcf.resource_groups.begin_export_template(resource_group, + parameters=export_template_request) + return result.template diff --git a/src/spring/azext_spring/spring_instance.py b/src/spring/azext_spring/spring_instance.py index 7e3b558ef60..171b0afcf92 100644 --- a/src/spring/azext_spring/spring_instance.py +++ b/src/spring/azext_spring/spring_instance.py @@ -27,6 +27,7 @@ from knack.log import get_logger from ._marketplace import _spring_list_marketplace_plan from ._constant import (MARKETPLACE_OFFER_ID, MARKETPLACE_PUBLISHER_ID, AKS_RP) +from .migration.migration_operations import migration_aca_start logger = get_logger(__name__) @@ -324,3 +325,10 @@ def spring_private_dns_zone_clean(cmd, client, resource_group, service): updated_resource = models.ServiceResource(location=resource.location, sku=resource.sku, properties=resource.properties, tags=resource.tags) return sdk_no_wait(False, client.services.begin_create_or_update, resource_group_name=resource_group, service_name=service, resource=updated_resource) + + +def spring_migration_start(cmd, client, resource_group, service, target="aca", output_folder=None): + if target == "aca" or target == "azure-container-apps": + migration_aca_start(cmd, client, resource_group, service, output_folder) + else: + raise InvalidArgumentValueError("Invalid target value. The value must be 'aca' or 'azure-container-apps'.") diff --git a/src/spring/azext_spring/tests/latest/recordings/test_asa_export.yaml b/src/spring/azext_spring/tests/latest/recordings/test_asa_export.yaml new file mode 100644 index 00000000000..32d7c2b9774 --- /dev/null +++ b/src/spring/azext_spring/tests/latest/recordings/test_asa_export.yaml @@ -0,0 +1,294 @@ +interactions: +- request: + body: '{"resources": ["/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.AppPlatform/Spring/clitest000002"], + "options": "SkipAllParameterization,IncludeParameterDefaultValue"}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - spring export + Connection: + - keep-alive + Content-Length: + - '213' + Content-Type: + - application/json + ParameterSetName: + - -g -s --subscription --output-folder --debug --verbose + User-Agent: + - AZURECLI/2.68.0 azsdk-python-core/1.31.0 Python/3.10.11 (Windows-10-10.0.26100-SP0) + method: POST + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/exportTemplate?api-version=2022-09-01 + response: + body: + string: '' + headers: + cache-control: + - no-cache + content-length: + - '0' + date: + - Mon, 10 Mar 2025 14:30:06 GMT + expires: + - '-1' + location: + - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/operationresults/eyJqb2JJZCI6IkV4cG9ydFRlbXBsYXRlSm9iLU5JTlBBTi1DRTdCNjU5RjoyRDVCN0Z8MzU5RjU3MzVCNkNCQkNFOSIsImpvYkxvY2F0aW9uIjoidWtzb3V0aCJ9?api-version=2022-09-01&t=638772138063152846&c=MIIHpTCCBo2gAwIBAgITfwTbn828Ducmmj24MgAEBNufzTANBgkqhkiG9w0BAQsFADBEMRMwEQYKCZImiZPyLGQBGRYDR0JMMRMwEQYKCZImiZPyLGQBGRYDQU1FMRgwFgYDVQQDEw9BTUUgSW5mcmEgQ0EgMDIwHhcNMjUwMTI1MTI1MTUzWhcNMjUwNzI0MTI1MTUzWjBAMT4wPAYDVQQDEzVhc3luY29wZXJhdGlvbnNpZ25pbmdjZXJ0aWZpY2F0ZS5tYW5hZ2VtZW50LmF6dXJlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL20TJQJbhV5Jrwzn-fiGrag_COjmaTwDy9Ir0oe1CLIfiJ9ageBVfcEmW-k5bUVL3eg6B8mQTEYE-FJDVVZ4jbJ9Qw8REpm2kBASDRwoItVVD_HBpJf1VhdViEPJPMDvLg0mAmde0X2m3HVEO6Y7eggJ9iL31DDv9PF-Xvn6x9xlWvO3_OCJReOoV_HCTDyzds4Pq9OySlnAGAozKYzOumbcVPz_WEMc_vwW80fjQLmdihJgp6_15qlnMdx48MQhVGT3y4gdbknMQJghyzTFcsASVncSqtmz8nAx5qT9dZ63iaF6E7Fbx76fnF4lx5K72ANX5cjlfVOig5jzgf8RPkCAwEAAaOCBJIwggSOMCcGCSsGAQQBgjcVCgQaMBgwCgYIKwYBBQUHAwEwCgYIKwYBBQUHAwIwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhpDjDYTVtHiE8Ys-hZvdFs6dEoFghfmRS4WsmTQCAWQCAQcwggHaBggrBgEFBQcBAQSCAcwwggHIMGYGCCsGAQUFBzAChlpodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpaW5mcmEvQ2VydHMvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmwxLmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MFYGCCsGAQUFBzAChkpodHRwOi8vY3JsMi5hbWUuZ2JsL2FpYS9CTDJQS0lJTlRDQTAxLkFNRS5HQkxfQU1FJTIwSW5mcmElMjBDQSUyMDAyKDQpLmNydDBWBggrBgEFBQcwAoZKaHR0cDovL2NybDMuYW1lLmdibC9haWEvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmw0LmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MB0GA1UdDgQWBBQp8DW_okjTMbIBWANCvQr_FrvzazAOBgNVHQ8BAf8EBAMCBaAwggE1BgNVHR8EggEsMIIBKDCCASSgggEgoIIBHIZCaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraWluZnJhL0NSTC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMS5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMi5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMy5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsNC5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JsMIGdBgNVHSAEgZUwgZIwDAYKKwYBBAGCN3sBATBmBgorBgEEAYI3ewICMFgwVgYIKwYBBQUHAgIwSh5IADMAMwBlADAAMQA5ADIAMQAtADQAZAA2ADQALQA0AGYAOABjAC0AYQAwADUANQAtADUAYgBkAGEAZgBmAGQANQBlADMAMwBkMAwGCisGAQQBgjd7AwIwDAYKKwYBBAGCN3sEAjAfBgNVHSMEGDAWgBSuecJrXSWIEwb2BwnDl3x7l48dVTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAA5sOyKXcQwQQAVKz9J8GIjVymZkoHVSft4TqdgNxSdoIAYkk_tYdx_dEZChJfSvIfzhzAb8k57EHRKNwKtP752SSUr0Q0oB60Y3Fq7il1fMKFTbVk9ZMTyOoo3hJmRwJaisv9rK2UVHWvwD2iUKD0IK_tHwy3m6bqbGDVKaRn1K9UYM39wEvEdy-k8J2z3Olfn6yYpcrVBHWzDzSy7TVdgUzaa0IZ670aJGPrNVYMvsCepP2_T_FdHVk4LoK9K4_0-GkZbvBLZPQO6FYgttg78s6Nn34TUcXWeTeeXArlkf48rbeL5fDY_CJyKYXLv3arwG7gUdcU5T8MGHeLLzcyo&s=jtmXu36pC5gfaKZpivfN2UqAQImUbvV6TLXx9gFHxeW81-Paz3rTsMuHT9JE3QH5C1Fx6ofLfhprvyGdva9RcSApI9jqCTgHUCy6NNQQLA8zMDpDwV56hzDIrObHLinGf6nYYc-Vatf1to-gS--zWgRkVm3RLntqkczMcuHs7vU_uT4y-yy7aHy3s5UIMDJb1VBtpf7GgmJUFdrxJzOqPYbCUqi2MklA0U3rzlFoPItrN9tvlDn_8rZzb4DAsU_i0D2rfWOFQ7-u2Fa1ysJ5e0sR0ZE82zHJssKlDfwJH0hizJsgAUsEOujpUGrhbU58mLPz5aAW3ohTpkfCcvCxpQ&h=PLJzU6QQ4DuQgDVgvriYzWFToM9DX0cD_ZKh4Ao8yeY + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + x-cache: + - CONFIG_NOCACHE + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-global-reads: + - '16500' + x-msedge-ref: + - 'Ref A: 72559C27E437464AA9411953B1625E90 Ref B: MAA201060513025 Ref C: 2025-03-10T14:29:57Z' + status: + code: 202 + message: Accepted +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - spring export + Connection: + - keep-alive + ParameterSetName: + - -g -s --subscription --output-folder --debug --verbose + User-Agent: + - AZURECLI/2.68.0 azsdk-python-core/1.31.0 Python/3.10.11 (Windows-10-10.0.26100-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/operationresults/eyJqb2JJZCI6IkV4cG9ydFRlbXBsYXRlSm9iLU5JTlBBTi1DRTdCNjU5RjoyRDVCN0Z8MzU5RjU3MzVCNkNCQkNFOSIsImpvYkxvY2F0aW9uIjoidWtzb3V0aCJ9?api-version=2022-09-01&t=638772138063152846&c=MIIHpTCCBo2gAwIBAgITfwTbn828Ducmmj24MgAEBNufzTANBgkqhkiG9w0BAQsFADBEMRMwEQYKCZImiZPyLGQBGRYDR0JMMRMwEQYKCZImiZPyLGQBGRYDQU1FMRgwFgYDVQQDEw9BTUUgSW5mcmEgQ0EgMDIwHhcNMjUwMTI1MTI1MTUzWhcNMjUwNzI0MTI1MTUzWjBAMT4wPAYDVQQDEzVhc3luY29wZXJhdGlvbnNpZ25pbmdjZXJ0aWZpY2F0ZS5tYW5hZ2VtZW50LmF6dXJlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL20TJQJbhV5Jrwzn-fiGrag_COjmaTwDy9Ir0oe1CLIfiJ9ageBVfcEmW-k5bUVL3eg6B8mQTEYE-FJDVVZ4jbJ9Qw8REpm2kBASDRwoItVVD_HBpJf1VhdViEPJPMDvLg0mAmde0X2m3HVEO6Y7eggJ9iL31DDv9PF-Xvn6x9xlWvO3_OCJReOoV_HCTDyzds4Pq9OySlnAGAozKYzOumbcVPz_WEMc_vwW80fjQLmdihJgp6_15qlnMdx48MQhVGT3y4gdbknMQJghyzTFcsASVncSqtmz8nAx5qT9dZ63iaF6E7Fbx76fnF4lx5K72ANX5cjlfVOig5jzgf8RPkCAwEAAaOCBJIwggSOMCcGCSsGAQQBgjcVCgQaMBgwCgYIKwYBBQUHAwEwCgYIKwYBBQUHAwIwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhpDjDYTVtHiE8Ys-hZvdFs6dEoFghfmRS4WsmTQCAWQCAQcwggHaBggrBgEFBQcBAQSCAcwwggHIMGYGCCsGAQUFBzAChlpodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpaW5mcmEvQ2VydHMvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmwxLmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MFYGCCsGAQUFBzAChkpodHRwOi8vY3JsMi5hbWUuZ2JsL2FpYS9CTDJQS0lJTlRDQTAxLkFNRS5HQkxfQU1FJTIwSW5mcmElMjBDQSUyMDAyKDQpLmNydDBWBggrBgEFBQcwAoZKaHR0cDovL2NybDMuYW1lLmdibC9haWEvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmw0LmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MB0GA1UdDgQWBBQp8DW_okjTMbIBWANCvQr_FrvzazAOBgNVHQ8BAf8EBAMCBaAwggE1BgNVHR8EggEsMIIBKDCCASSgggEgoIIBHIZCaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraWluZnJhL0NSTC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMS5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMi5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMy5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsNC5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JsMIGdBgNVHSAEgZUwgZIwDAYKKwYBBAGCN3sBATBmBgorBgEEAYI3ewICMFgwVgYIKwYBBQUHAgIwSh5IADMAMwBlADAAMQA5ADIAMQAtADQAZAA2ADQALQA0AGYAOABjAC0AYQAwADUANQAtADUAYgBkAGEAZgBmAGQANQBlADMAMwBkMAwGCisGAQQBgjd7AwIwDAYKKwYBBAGCN3sEAjAfBgNVHSMEGDAWgBSuecJrXSWIEwb2BwnDl3x7l48dVTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAA5sOyKXcQwQQAVKz9J8GIjVymZkoHVSft4TqdgNxSdoIAYkk_tYdx_dEZChJfSvIfzhzAb8k57EHRKNwKtP752SSUr0Q0oB60Y3Fq7il1fMKFTbVk9ZMTyOoo3hJmRwJaisv9rK2UVHWvwD2iUKD0IK_tHwy3m6bqbGDVKaRn1K9UYM39wEvEdy-k8J2z3Olfn6yYpcrVBHWzDzSy7TVdgUzaa0IZ670aJGPrNVYMvsCepP2_T_FdHVk4LoK9K4_0-GkZbvBLZPQO6FYgttg78s6Nn34TUcXWeTeeXArlkf48rbeL5fDY_CJyKYXLv3arwG7gUdcU5T8MGHeLLzcyo&s=jtmXu36pC5gfaKZpivfN2UqAQImUbvV6TLXx9gFHxeW81-Paz3rTsMuHT9JE3QH5C1Fx6ofLfhprvyGdva9RcSApI9jqCTgHUCy6NNQQLA8zMDpDwV56hzDIrObHLinGf6nYYc-Vatf1to-gS--zWgRkVm3RLntqkczMcuHs7vU_uT4y-yy7aHy3s5UIMDJb1VBtpf7GgmJUFdrxJzOqPYbCUqi2MklA0U3rzlFoPItrN9tvlDn_8rZzb4DAsU_i0D2rfWOFQ7-u2Fa1ysJ5e0sR0ZE82zHJssKlDfwJH0hizJsgAUsEOujpUGrhbU58mLPz5aAW3ohTpkfCcvCxpQ&h=PLJzU6QQ4DuQgDVgvriYzWFToM9DX0cD_ZKh4Ao8yeY + response: + body: + string: '' + headers: + cache-control: + - no-cache + content-length: + - '0' + date: + - Mon, 10 Mar 2025 14:30:07 GMT + expires: + - '-1' + location: + - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/operationresults/eyJqb2JJZCI6IkV4cG9ydFRlbXBsYXRlSm9iLU5JTlBBTi1DRTdCNjU5RjoyRDVCN0Z8MzU5RjU3MzVCNkNCQkNFOSIsImpvYkxvY2F0aW9uIjoidWtzb3V0aCJ9?api-version=2022-09-01&t=638772138073128198&c=MIIHpTCCBo2gAwIBAgITfwTbn828Ducmmj24MgAEBNufzTANBgkqhkiG9w0BAQsFADBEMRMwEQYKCZImiZPyLGQBGRYDR0JMMRMwEQYKCZImiZPyLGQBGRYDQU1FMRgwFgYDVQQDEw9BTUUgSW5mcmEgQ0EgMDIwHhcNMjUwMTI1MTI1MTUzWhcNMjUwNzI0MTI1MTUzWjBAMT4wPAYDVQQDEzVhc3luY29wZXJhdGlvbnNpZ25pbmdjZXJ0aWZpY2F0ZS5tYW5hZ2VtZW50LmF6dXJlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL20TJQJbhV5Jrwzn-fiGrag_COjmaTwDy9Ir0oe1CLIfiJ9ageBVfcEmW-k5bUVL3eg6B8mQTEYE-FJDVVZ4jbJ9Qw8REpm2kBASDRwoItVVD_HBpJf1VhdViEPJPMDvLg0mAmde0X2m3HVEO6Y7eggJ9iL31DDv9PF-Xvn6x9xlWvO3_OCJReOoV_HCTDyzds4Pq9OySlnAGAozKYzOumbcVPz_WEMc_vwW80fjQLmdihJgp6_15qlnMdx48MQhVGT3y4gdbknMQJghyzTFcsASVncSqtmz8nAx5qT9dZ63iaF6E7Fbx76fnF4lx5K72ANX5cjlfVOig5jzgf8RPkCAwEAAaOCBJIwggSOMCcGCSsGAQQBgjcVCgQaMBgwCgYIKwYBBQUHAwEwCgYIKwYBBQUHAwIwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhpDjDYTVtHiE8Ys-hZvdFs6dEoFghfmRS4WsmTQCAWQCAQcwggHaBggrBgEFBQcBAQSCAcwwggHIMGYGCCsGAQUFBzAChlpodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpaW5mcmEvQ2VydHMvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmwxLmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MFYGCCsGAQUFBzAChkpodHRwOi8vY3JsMi5hbWUuZ2JsL2FpYS9CTDJQS0lJTlRDQTAxLkFNRS5HQkxfQU1FJTIwSW5mcmElMjBDQSUyMDAyKDQpLmNydDBWBggrBgEFBQcwAoZKaHR0cDovL2NybDMuYW1lLmdibC9haWEvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmw0LmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MB0GA1UdDgQWBBQp8DW_okjTMbIBWANCvQr_FrvzazAOBgNVHQ8BAf8EBAMCBaAwggE1BgNVHR8EggEsMIIBKDCCASSgggEgoIIBHIZCaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraWluZnJhL0NSTC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMS5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMi5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMy5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsNC5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JsMIGdBgNVHSAEgZUwgZIwDAYKKwYBBAGCN3sBATBmBgorBgEEAYI3ewICMFgwVgYIKwYBBQUHAgIwSh5IADMAMwBlADAAMQA5ADIAMQAtADQAZAA2ADQALQA0AGYAOABjAC0AYQAwADUANQAtADUAYgBkAGEAZgBmAGQANQBlADMAMwBkMAwGCisGAQQBgjd7AwIwDAYKKwYBBAGCN3sEAjAfBgNVHSMEGDAWgBSuecJrXSWIEwb2BwnDl3x7l48dVTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAA5sOyKXcQwQQAVKz9J8GIjVymZkoHVSft4TqdgNxSdoIAYkk_tYdx_dEZChJfSvIfzhzAb8k57EHRKNwKtP752SSUr0Q0oB60Y3Fq7il1fMKFTbVk9ZMTyOoo3hJmRwJaisv9rK2UVHWvwD2iUKD0IK_tHwy3m6bqbGDVKaRn1K9UYM39wEvEdy-k8J2z3Olfn6yYpcrVBHWzDzSy7TVdgUzaa0IZ670aJGPrNVYMvsCepP2_T_FdHVk4LoK9K4_0-GkZbvBLZPQO6FYgttg78s6Nn34TUcXWeTeeXArlkf48rbeL5fDY_CJyKYXLv3arwG7gUdcU5T8MGHeLLzcyo&s=DmoScDwY0DC9v75MM1-hHXIU8TowuTQLwT1pfoeqw_lbQSVZD3FrHKc8jAGMtITFDGKVJUhKdWejTfuwUqzidmKP4XS2tbjw-Rn6X5cAL3kZ6kjZ1Jh0vIj88kzo28P9iV70l_HyaULt30c20O0C_gJzMWwPdM5cKBXqb7qT6c529XJHzRCL6q4I6y7GsOtYoO_v0fqwN-Sv4ubhEyH0gC5B8ncccvxomYipGIBibWSJ54x8EJJTVmMrtwvy726Fb39_7qRGb-WBt7EmwHUL78uzaxW0lUkSdGeFqWzlOaOWF3qD8I-4HSifHXpmLFmhLZg8yVmU9k1Esj_zBFG6Mg&h=iEG5lzUN_bT4I-e5kP3M9xQO6S5L2hywzTVRIFxLKTY + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + x-cache: + - CONFIG_NOCACHE + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-global-reads: + - '16499' + x-msedge-ref: + - 'Ref A: 6180A50D75A64F2DA36DA2E873B47BBF Ref B: MAA201060513025 Ref C: 2025-03-10T14:30:06Z' + status: + code: 202 + message: Accepted +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - spring export + Connection: + - keep-alive + ParameterSetName: + - -g -s --subscription --output-folder --debug --verbose + User-Agent: + - AZURECLI/2.68.0 azsdk-python-core/1.31.0 Python/3.10.11 (Windows-10-10.0.26100-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/operationresults/eyJqb2JJZCI6IkV4cG9ydFRlbXBsYXRlSm9iLU5JTlBBTi1DRTdCNjU5RjoyRDVCN0Z8MzU5RjU3MzVCNkNCQkNFOSIsImpvYkxvY2F0aW9uIjoidWtzb3V0aCJ9?api-version=2022-09-01&t=638772138073128198&c=MIIHpTCCBo2gAwIBAgITfwTbn828Ducmmj24MgAEBNufzTANBgkqhkiG9w0BAQsFADBEMRMwEQYKCZImiZPyLGQBGRYDR0JMMRMwEQYKCZImiZPyLGQBGRYDQU1FMRgwFgYDVQQDEw9BTUUgSW5mcmEgQ0EgMDIwHhcNMjUwMTI1MTI1MTUzWhcNMjUwNzI0MTI1MTUzWjBAMT4wPAYDVQQDEzVhc3luY29wZXJhdGlvbnNpZ25pbmdjZXJ0aWZpY2F0ZS5tYW5hZ2VtZW50LmF6dXJlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL20TJQJbhV5Jrwzn-fiGrag_COjmaTwDy9Ir0oe1CLIfiJ9ageBVfcEmW-k5bUVL3eg6B8mQTEYE-FJDVVZ4jbJ9Qw8REpm2kBASDRwoItVVD_HBpJf1VhdViEPJPMDvLg0mAmde0X2m3HVEO6Y7eggJ9iL31DDv9PF-Xvn6x9xlWvO3_OCJReOoV_HCTDyzds4Pq9OySlnAGAozKYzOumbcVPz_WEMc_vwW80fjQLmdihJgp6_15qlnMdx48MQhVGT3y4gdbknMQJghyzTFcsASVncSqtmz8nAx5qT9dZ63iaF6E7Fbx76fnF4lx5K72ANX5cjlfVOig5jzgf8RPkCAwEAAaOCBJIwggSOMCcGCSsGAQQBgjcVCgQaMBgwCgYIKwYBBQUHAwEwCgYIKwYBBQUHAwIwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhpDjDYTVtHiE8Ys-hZvdFs6dEoFghfmRS4WsmTQCAWQCAQcwggHaBggrBgEFBQcBAQSCAcwwggHIMGYGCCsGAQUFBzAChlpodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpaW5mcmEvQ2VydHMvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmwxLmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MFYGCCsGAQUFBzAChkpodHRwOi8vY3JsMi5hbWUuZ2JsL2FpYS9CTDJQS0lJTlRDQTAxLkFNRS5HQkxfQU1FJTIwSW5mcmElMjBDQSUyMDAyKDQpLmNydDBWBggrBgEFBQcwAoZKaHR0cDovL2NybDMuYW1lLmdibC9haWEvQkwyUEtJSU5UQ0EwMS5BTUUuR0JMX0FNRSUyMEluZnJhJTIwQ0ElMjAwMig0KS5jcnQwVgYIKwYBBQUHMAKGSmh0dHA6Ly9jcmw0LmFtZS5nYmwvYWlhL0JMMlBLSUlOVENBMDEuQU1FLkdCTF9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3J0MB0GA1UdDgQWBBQp8DW_okjTMbIBWANCvQr_FrvzazAOBgNVHQ8BAf8EBAMCBaAwggE1BgNVHR8EggEsMIIBKDCCASSgggEgoIIBHIZCaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraWluZnJhL0NSTC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMS5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMi5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsMy5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JshjRodHRwOi8vY3JsNC5hbWUuZ2JsL2NybC9BTUUlMjBJbmZyYSUyMENBJTIwMDIoNCkuY3JsMIGdBgNVHSAEgZUwgZIwDAYKKwYBBAGCN3sBATBmBgorBgEEAYI3ewICMFgwVgYIKwYBBQUHAgIwSh5IADMAMwBlADAAMQA5ADIAMQAtADQAZAA2ADQALQA0AGYAOABjAC0AYQAwADUANQAtADUAYgBkAGEAZgBmAGQANQBlADMAMwBkMAwGCisGAQQBgjd7AwIwDAYKKwYBBAGCN3sEAjAfBgNVHSMEGDAWgBSuecJrXSWIEwb2BwnDl3x7l48dVTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAA5sOyKXcQwQQAVKz9J8GIjVymZkoHVSft4TqdgNxSdoIAYkk_tYdx_dEZChJfSvIfzhzAb8k57EHRKNwKtP752SSUr0Q0oB60Y3Fq7il1fMKFTbVk9ZMTyOoo3hJmRwJaisv9rK2UVHWvwD2iUKD0IK_tHwy3m6bqbGDVKaRn1K9UYM39wEvEdy-k8J2z3Olfn6yYpcrVBHWzDzSy7TVdgUzaa0IZ670aJGPrNVYMvsCepP2_T_FdHVk4LoK9K4_0-GkZbvBLZPQO6FYgttg78s6Nn34TUcXWeTeeXArlkf48rbeL5fDY_CJyKYXLv3arwG7gUdcU5T8MGHeLLzcyo&s=DmoScDwY0DC9v75MM1-hHXIU8TowuTQLwT1pfoeqw_lbQSVZD3FrHKc8jAGMtITFDGKVJUhKdWejTfuwUqzidmKP4XS2tbjw-Rn6X5cAL3kZ6kjZ1Jh0vIj88kzo28P9iV70l_HyaULt30c20O0C_gJzMWwPdM5cKBXqb7qT6c529XJHzRCL6q4I6y7GsOtYoO_v0fqwN-Sv4ubhEyH0gC5B8ncccvxomYipGIBibWSJ54x8EJJTVmMrtwvy726Fb39_7qRGb-WBt7EmwHUL78uzaxW0lUkSdGeFqWzlOaOWF3qD8I-4HSifHXpmLFmhLZg8yVmU9k1Esj_zBFG6Mg&h=iEG5lzUN_bT4I-e5kP3M9xQO6S5L2hywzTVRIFxLKTY + response: + body: + string: '{"template":{"$schema":"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#","contentVersion":"1.0.0.0","parameters":{},"variables":{},"resources":[{"type":"Microsoft.AppPlatform/Spring","apiVersion":"2024-05-01-preview","name":"clitest000002","location":"eastus","sku":{"name":"E0","tier":"Enterprise"},"properties":{"zoneRedundant":false,"maintenanceScheduleConfiguration":{"frequency":"Weekly","day":"Wednesday","hour":1},"networkProfile":{"outboundType":"loadBalancer"},"marketplaceResource":{"plan":"asa-ent-hr-mtr","publisher":"vmware-inc","product":"azure-spring-cloud-vmware-tanzu-2"}}},{"type":"Microsoft.AppPlatform/Spring/applicationAccelerators","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{}},{"type":"Microsoft.AppPlatform/Spring/applicationLiveViews","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/app-domain","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":false,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/app-sys-mi","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":false,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/app-system-assigned-mi","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":false,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/app-with-secret","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":false,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/oomapp","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":true,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"clientAuth":{"certificates":[]},"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/undefined","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":true,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/buildServices","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"resourceRequests":{}}},{"type":"Microsoft.AppPlatform/Spring/certificates","apiVersion":"2024-05-01-preview","name":"clitest000002/195076f11be","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"vaultUri":"https://integration-test-prod.vault.azure.net","keyVaultCertName":"pfx-cert","certVersion":"a1928e36a5ac46f78cde456b72867a72","excludePrivateKey":false,"autoSync":"Disabled","type":"KeyVaultCertificate"}},{"type":"Microsoft.AppPlatform/Spring/certificates","apiVersion":"2024-05-01-preview","name":"clitest000002/request","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"type":"ContentCertificate"}},{"type":"Microsoft.AppPlatform/Spring/certificates","apiVersion":"2024-05-01-preview","name":"clitest000002/uploaded-cert","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"type":"ContentCertificate"}},{"type":"Microsoft.AppPlatform/Spring/configServers","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"configServer":{"gitProperty":{"repositories":[{"name":"test1","pattern":["application/default"],"uri":"git@bitbucket.org:ms-azdmss-it/config-server-test-ssh-repo.git","label":"master","searchPaths":["/"],"hostKey":"*","hostKeyAlgorithm":"*","privateKey":"*","strictHostKeyChecking":false}],"uri":"https://github.com/Azure-Samples/acme-fitness-store-config","label":"main","searchPaths":[],"username":"*","password":"*"}}}},{"type":"Microsoft.AppPlatform/Spring/configurationServices","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"settings":{"gitProperty":{"repositories":[{"name":"repo1","patterns":["customers-service"],"label":"master","gitImplementation":"go-git","uri":"https://github.com/leonard520/spring-petclinic-microservices-config","searchPaths":[]},{"name":"repo2","patterns":["application"],"label":"master","gitImplementation":"go-git","uri":"git@bitbucket.org:ms-azdmss-it/config-server-test-ssh-repo.git","searchPaths":["/","/hello"],"hostKey":"*","hostKeyAlgorithm":"*","privateKey":"*"},{"name":"repo3","patterns":["gateway"],"label":"main","gitImplementation":"go-git","uri":"https://gitlab.com/ms-azdmss-it/config-server-test-https-repo.git","searchPaths":["/abc"],"username":"*","password":"*"}]}},"generation":"Gen2"}},{"type":"Microsoft.AppPlatform/Spring/devToolPortals","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"public":false,"features":{"applicationAccelerator":{"state":"Enabled"},"applicationLiveView":{"state":"Enabled"}}}},{"type":"Microsoft.AppPlatform/Spring/gateways","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":2},"properties":{"public":false,"httpsOnly":false,"apiMetadataProperties":{},"corsProperties":{"allowedOrigins":["*"],"allowedOriginPatterns":["*"],"allowedMethods":["*"],"allowedHeaders":["*"],"maxAge":3600,"allowCredentials":true,"exposedHeaders":["true"]},"resourceRequests":{"cpu":"1","memory":"2Gi"},"environmentVariables":{"properties":{"spring.cloud.gateway.httpclient.pool.metrics":"false"}},"clientAuth":{"certificateVerification":"Disabled"}}},{"type":"Microsoft.AppPlatform/Spring/serviceRegistries","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"]},{"type":"Microsoft.AppPlatform/Spring/storages","apiVersion":"2024-05-01-preview","name":"clitest000002/sntest1","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"accountName":"test","storageType":"StorageAccount","accountKey":null}},{"type":"Microsoft.AppPlatform/Spring/storages","apiVersion":"2024-05-01-preview","name":"clitest000002/sntestsa1","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"accountName":"testsa","storageType":"StorageAccount","accountKey":null}},{"type":"Microsoft.AppPlatform/Spring/storages","apiVersion":"2024-05-01-preview","name":"clitest000002/storage3","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"accountName":"testsatest","storageType":"StorageAccount","accountKey":null}},{"type":"Microsoft.AppPlatform/Spring/apiPortals","apiVersion":"2024-05-01-preview","name":"clitest000002/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/gateways'', + ''clitest000002'', ''default'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"public":true,"httpsOnly":false,"gatewayIds":["[resourceId(''Microsoft.AppPlatform/Spring/gateways'', + ''clitest000002'', ''default'')]"],"apiTryOutEnabledState":"Enabled"}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/catalog","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/configServers'', + ''clitest000002'', ''default'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{"resourceId":"[resourceId(''Microsoft.AppPlatform/Spring/configServers'', + ''clitest000002'', ''default'')]"},"serviceRegistry":{}},"public":false,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"clientAuth":{"certificates":[]},"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":86400,"sessionAffinity":"Cookie","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/testmount","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/storages'', + ''clitest000002'', ''storage3'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{},"serviceRegistry":{}},"public":false,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"customPersistentDisks":[{"storageId":"[resourceId(''Microsoft.AppPlatform/Spring/storages'', + ''clitest000002'', ''storage3'')]","customPersistentDiskProperties":{"type":"AzureFileVolume","shareName":"guitarshare","mountPath":"/test/gutiar3","readOnly":false,"enableSubPath":false}}],"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"clientAuth":{"certificates":[]},"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/app-domain/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''app-domain'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"terminationGracePeriodSeconds":90,"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":""}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/app-sys-mi/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''app-sys-mi'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"terminationGracePeriodSeconds":90,"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":""}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/app-system-assigned-mi/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''app-system-assigned-mi'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"terminationGracePeriodSeconds":90,"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":""}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/app-with-secret/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''app-with-secret'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"environmentVariables":{"env-key":"abc","env-secret-key":"def"},"terminationGracePeriodSeconds":90,"startupProbe":{"disableProbe":true},"livenessProbe":{"disableProbe":true},"readinessProbe":{"disableProbe":true}},"active":true,"source":{"type":"BuildResult","buildResultId":""}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/hello-world-app/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''hello-world-app'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"2Gi"},"environmentVariables":{"SERVER_PORT":"8080","aa":"aa"},"terminationGracePeriodSeconds":90,"startupProbe":{"disableProbe":true},"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"Container","customContainer":{"containerImage":"azurespringapps/samples/hello-world:0.0.1","server":"mcr.microsoft.com","command":[],"args":[],"languageFramework":"springboot"}}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/testmount/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''testmount'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"environmentVariables":{},"terminationGracePeriodSeconds":90,"startupProbe":{"disableProbe":true},"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":""}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/undefined/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''undefined'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"500m","memory":"1Gi"},"terminationGracePeriodSeconds":90,"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":""}}},{"type":"Microsoft.AppPlatform/Spring/apps/domains","apiVersion":"2024-05-01-preview","name":"clitest000002/app-domain/whatever.azdmss-test.net","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''app-domain'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"thumbprint":"dee0e7dda4d3d42fcb43f765015c682fab4cbb78","certName":"195076f11be"}},{"type":"Microsoft.AppPlatform/Spring/buildServices/agentPools","apiVersion":"2024-05-01-preview","name":"clitest000002/default/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/buildServices'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"poolSize":{"name":"S1"}}},{"type":"Microsoft.AppPlatform/Spring/buildServices/builders","apiVersion":"2024-05-01-preview","name":"clitest000002/default/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/buildServices'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"stack":{"id":"io.buildpacks.stacks.jammy","version":"base"},"buildpackGroups":[{"name":"default","buildpacks":[{"id":"tanzu-buildpacks/java-azure"},{"id":"tanzu-buildpacks/dotnet-core"},{"id":"tanzu-buildpacks/go"},{"id":"tanzu-buildpacks/web-servers"},{"id":"tanzu-buildpacks/nodejs"},{"id":"tanzu-buildpacks/python"}]}]}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/catalog/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''catalog'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builds'', + ''clitest000002'', ''default'', ''catalog-default'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"environmentVariables":{},"terminationGracePeriodSeconds":90,"startupProbe":{"disableProbe":true},"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":"[concat(resourceId(''Microsoft.AppPlatform/Spring/buildServices/builds'', + ''clitest000002'', ''default'', ''catalog-default''), ''/results/1'')]"}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/oomapp/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''oomapp'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builds'', + ''clitest000002'', ''default'', ''oomapp-default'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"500m","memory":"512Mi"},"environmentVariables":{"JAVA_OPTS":"-XX:MaxRAM=512m"},"terminationGracePeriodSeconds":90,"startupProbe":{"disableProbe":true},"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":true,"source":{"type":"BuildResult","buildResultId":"[concat(resourceId(''Microsoft.AppPlatform/Spring/buildServices/builds'', + ''clitest000002'', ''default'', ''oomapp-default''), ''/results/5'')]"}}},{"type":"Microsoft.AppPlatform/Spring/apps/deployments","apiVersion":"2024-05-01-preview","name":"clitest000002/testmount/green","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''testmount'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builds'', + ''clitest000002'', ''default'', ''testmount-green'')]"],"sku":{"name":"E0","tier":"Enterprise","capacity":1},"properties":{"deploymentSettings":{"resourceRequests":{"cpu":"1","memory":"1Gi"},"environmentVariables":{},"terminationGracePeriodSeconds":90,"livenessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":300,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}},"readinessProbe":{"disableProbe":false,"failureThreshold":3,"initialDelaySeconds":0,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":3,"probeAction":{"type":"TCPSocketAction"}}},"active":false,"source":{"type":"BuildResult","buildResultId":"[concat(resourceId(''Microsoft.AppPlatform/Spring/buildServices/builds'', + ''clitest000002'', ''default'', ''testmount-green''), ''/results/1'')]"}}},{"type":"Microsoft.AppPlatform/Spring/buildServices/builders/buildpackBindings","apiVersion":"2024-05-01-preview","name":"clitest000002/default/default/default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]"],"properties":{"bindingType":"ApplicationInsights","launchProperties":{"properties":{"sampling_percentage":"10","connection_string":"InstrumentationKey=3fc9aee4-ffe9-4cd2-b8d3-f5ac688b7019;IngestionEndpoint=https://eastus-6.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=c633c0af-4813-496c-9296-5b902ed0fe77"}}}},{"type":"Microsoft.AppPlatform/Spring/gateways/routeConfigs","apiVersion":"2024-05-01-preview","name":"clitest000002/default/customers-service-rule","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/gateways'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''hello-world-app'')]"],"properties":{"appResourceId":"[resourceId(''Microsoft.AppPlatform/Spring/apps'', + ''clitest000002'', ''hello-world-app'')]","protocol":"HTTP","routes":[{"title":"Customers + service","description":"Route to customer service","ssoEnabled":false,"tokenRelay":false,"predicates":["Path=/api/customers-service"],"filters":["StripPrefix=2","Cors=[allowedOriginPatterns:http://xiading-vnet9-apiportal.azdmss-test.net,allowedMethods:GET;POST;DELETE,allowedHeaders:*]"],"tags":["pet + clinic"]}]}},{"type":"Microsoft.AppPlatform/Spring/apps","apiVersion":"2024-05-01-preview","name":"clitest000002/hello-world-app","location":"eastus","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/configServers'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring/storages'', + ''clitest000002'', ''sntestsa1'')]","[resourceId(''Microsoft.AppPlatform/Spring/storages'', + ''clitest000002'', ''sntest1'')]"],"properties":{"addonConfigs":{"applicationConfigurationService":{},"configServer":{"resourceId":"[resourceId(''Microsoft.AppPlatform/Spring/configServers'', + ''clitest000002'', ''default'')]"},"serviceRegistry":{}},"public":true,"httpsOnly":false,"temporaryDisk":{"sizeInGB":5,"mountPath":"/tmp"},"persistentDisk":{"sizeInGB":0,"mountPath":"/persistent"},"customPersistentDisks":[{"storageId":"[resourceId(''Microsoft.AppPlatform/Spring/storages'', + ''clitest000002'', ''sntestsa1'')]","customPersistentDiskProperties":{"type":"AzureFileVolume","shareName":"sashare1","mountPath":"/test/sashare1","readOnly":true,"enableSubPath":false}},{"storageId":"[resourceId(''Microsoft.AppPlatform/Spring/storages'', + ''clitest000002'', ''sntest1'')]","customPersistentDiskProperties":{"type":"AzureFileVolume","shareName":"npshare1","mountPath":"/test/share1","readOnly":false,"enableSubPath":true}}],"enableEndToEndTLS":false,"testEndpointAuthState":"Enabled","ingressSettings":{"clientAuth":{"certificates":[]},"readTimeoutInSeconds":300,"sendTimeoutInSeconds":60,"sessionCookieMaxAge":0,"sessionAffinity":"None","backendProtocol":"Default"}}},{"type":"Microsoft.AppPlatform/Spring/buildServices/builds","apiVersion":"2024-05-01-preview","name":"clitest000002/default/catalog-default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/buildServices'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/agentPools'', + ''clitest000002'', ''default'', ''default'')]"],"properties":{"builder":"[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","agentPool":"[resourceId(''Microsoft.AppPlatform/Spring/buildServices/agentPools'', + ''clitest000002'', ''default'', ''default'')]","relativePath":"resources/2024111105-ee1f6b48-9c1e-468d-817f-06e793d75eed","resourceRequests":{"cpu":"1","memory":"2Gi"},"env":{"BP_JVM_VERSION":"17"}}},{"type":"Microsoft.AppPlatform/Spring/buildServices/builds","apiVersion":"2024-05-01-preview","name":"clitest000002/default/oomapp-default","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/buildServices'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/agentPools'', + ''clitest000002'', ''default'', ''default'')]"],"properties":{"builder":"[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","agentPool":"[resourceId(''Microsoft.AppPlatform/Spring/buildServices/agentPools'', + ''clitest000002'', ''default'', ''default'')]","relativePath":"resources/2024112907-d0d43ab9-b7c5-4b3b-b179-4b633c074df3","resourceRequests":{"cpu":"1","memory":"2Gi"},"env":{}}},{"type":"Microsoft.AppPlatform/Spring/buildServices/builds","apiVersion":"2024-05-01-preview","name":"clitest000002/default/testmount-green","dependsOn":["[resourceId(''Microsoft.AppPlatform/Spring/buildServices'', + ''clitest000002'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring'', + ''clitest000002'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","[resourceId(''Microsoft.AppPlatform/Spring/buildServices/agentPools'', + ''clitest000002'', ''default'', ''default'')]"],"properties":{"builder":"[resourceId(''Microsoft.AppPlatform/Spring/buildServices/builders'', + ''clitest000002'', ''default'', ''default'')]","agentPool":"[resourceId(''Microsoft.AppPlatform/Spring/buildServices/agentPools'', + ''clitest000002'', ''default'', ''default'')]","relativePath":"resources/2025030707-4511b0ad-5ab4-4b92-973f-b2675ea82bd6","resourceRequests":{"cpu":"1","memory":"2Gi"},"env":{}}}]}}' + headers: + cache-control: + - no-cache + content-length: + - '30121' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 10 Mar 2025 14:30:23 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + x-cache: + - CONFIG_NOCACHE + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-global-reads: + - '16499' + x-msedge-ref: + - 'Ref A: 19CD70F404AD44F0A90D075C5AA79018 Ref B: MAA201060513025 Ref C: 2025-03-10T14:30:22Z' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - spring export + Connection: + - keep-alive + Content-Length: + - '0' + ParameterSetName: + - -g -s --subscription --output-folder --debug --verbose + User-Agent: + - AZURECLI/2.68.0 azsdk-python-core/1.31.0 Python/3.10.11 (Windows-10-10.0.26100-SP0) + method: POST + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.AppPlatform/Spring/clitest000002/gateways/default/listEnvSecrets?api-version=2024-05-01-preview + response: + body: + string: '{"test":"secrettttt"}' + headers: + cache-control: + - no-cache + content-length: + - '21' + content-type: + - application/json + date: + - Mon, 10 Mar 2025 14:30:25 GMT + expires: + - '-1' + pragma: + - no-cache + request-context: + - appId=cid-v1:ccd65fc4-7cd4-497e-8dc8-9a76e9a43ae2 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-cache: + - CONFIG_NOCACHE + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-global-writes: + - '11999' + x-ms-ratelimit-remaining-subscription-writes: + - '799' + x-msedge-ref: + - 'Ref A: 26D4D1008CE94C24811B2149C3D8C7DB Ref B: MAA201060513011 Ref C: 2025-03-10T14:30:24Z' + x-rp-server-mvid: + - 839407a8-f315-46e9-b8c7-bf2bfc03eac6 + status: + code: 200 + message: OK +version: 1 diff --git a/src/spring/azext_spring/tests/latest/recordings/test_persistent_storage.yaml b/src/spring/azext_spring/tests/latest/recordings/test_persistent_storage.yaml index 1718eb077ef..54300e75876 100644 --- a/src/spring/azext_spring/tests/latest/recordings/test_persistent_storage.yaml +++ b/src/spring/azext_spring/tests/latest/recordings/test_persistent_storage.yaml @@ -17,7 +17,7 @@ interactions: User-Agent: - AZURECLI/2.61.0 azsdk-python-core/1.30.2 Python/3.8.10 (Windows-10-10.0.22631-SP0) method: POST - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Storage/storageAccounts/clitest000002/listKeys?api-version=2023-05-01&$expand=kerb + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Storage/storageAccounts/clitest000002/listKeys?api-version=2024-01-01&$expand=kerb response: body: string: '{"keys":[{"creationTime":"2024-07-02T07:19:20.1735345Z","keyName":"key1","value":"veryFakedStorageAccountKey==","permissions":"FULL"},{"creationTime":"2024-07-02T07:19:20.1735345Z","keyName":"key2","value":"veryFakedStorageAccountKey==","permissions":"FULL"}]}' diff --git a/src/spring/azext_spring/tests/latest/test_asa_export.py b/src/spring/azext_spring/tests/latest/test_asa_export.py new file mode 100644 index 00000000000..d9e921543aa --- /dev/null +++ b/src/spring/azext_spring/tests/latest/test_asa_export.py @@ -0,0 +1,34 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import (ScenarioTest, record_only) +from .custom_preparers import (SpringPreparer, SpringResourceGroupPreparer) +from .custom_dev_setting_constant import SpringTestEnvironmentEnum +from knack.log import get_logger + +logger = get_logger(__name__) + +# pylint: disable=line-too-long +# pylint: disable=too-many-lines +''' +Since the scenarios covered here depend on a Azure Spring service instance creation. +It cannot support live run. So mark it as record_only. +''' + +@record_only() +class ApidExportTest(ScenarioTest): + + @SpringResourceGroupPreparer( + dev_setting_name=SpringTestEnvironmentEnum.ENTERPRISE_WITH_TANZU['resource_group_name']) + @SpringPreparer(**SpringTestEnvironmentEnum.ENTERPRISE_WITH_TANZU['spring']) + def test_asa_export(self, resource_group, spring): + self.kwargs.update({ + 'serviceName': spring, + 'rg': resource_group, + }) + + self.cmd('spring export -g {rg} -s {serviceName} --output-folder .\\output') + + diff --git a/src/spring/setup.py b/src/spring/setup.py index bf76d1caacb..20fc782bacf 100644 --- a/src/spring/setup.py +++ b/src/spring/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '1.26.1' +VERSION = '1.27.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers @@ -33,7 +33,9 @@ ] # TODO: Add any additional SDK dependencies here -DEPENDENCIES = [] +DEPENDENCIES = [ + 'jinja2' +] with open('README.md', 'r', encoding='utf-8') as f: README = f.read() @@ -53,5 +55,8 @@ classifiers=CLASSIFIERS, packages=find_packages(), install_requires=DEPENDENCIES, - package_data={'azext_spring': ['azext_metadata.json']}, + package_data={ + 'azext_spring': ['azext_metadata.json'], + 'azext_spring.migration.converter': ['templates/*.j2'] + }, )