From b529fd002e3d7fc82cdaca8a02fcdad20b4b025a Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Fri, 10 Jan 2025 14:37:55 +0800 Subject: [PATCH 01/98] init commit for spring migration start command --- src/spring/azext_spring/_help.py | 13 ++ src/spring/azext_spring/_params.py | 3 + src/spring/azext_spring/commands.py | 4 + .../migration/migration_operations.py | 149 ++++++++++++++++++ .../migration/templates/app.bicep.j2 | 74 +++++++++ .../migration/templates/environment.bicep.j2 | 53 +++++++ .../migration/templates/main.bicep.j2 | 39 +++++ .../migration/templates/readme_template.j2 | 52 ++++++ src/spring/azext_spring/spring_instance.py | 5 + 9 files changed, 392 insertions(+) create mode 100644 src/spring/azext_spring/migration/migration_operations.py create mode 100644 src/spring/azext_spring/migration/templates/app.bicep.j2 create mode 100644 src/spring/azext_spring/migration/templates/environment.bicep.j2 create mode 100644 src/spring/azext_spring/migration/templates/main.bicep.j2 create mode 100644 src/spring/azext_spring/migration/templates/readme_template.j2 diff --git a/src/spring/azext_spring/_help.py b/src/spring/azext_spring/_help.py index 7e04a6e4ff8..d68428b994f 100644 --- a/src/spring/azext_spring/_help.py +++ b/src/spring/azext_spring/_help.py @@ -1809,3 +1809,16 @@ - name: Clean up private DNS zone with Azure Spring Apps. text: az spring private-dns-zone clean --service MyAzureSpringAppsInstance --resource-group MyResourceGroup """ + +helps['spring migration-aca'] = """ + type: group + short-summary: Commands to migrate from Azure Spring Apps to Azure Container Apps. +""" + +helps['spring migration-aca start'] = """ + type: command + short-summary: Commands to start migration from Azure Spring Apps to Azure Container Apps. + examples: + - name: Generate corresponding bicep files and readme doc to create Azure Container Apps service. + text: az spring migration-aca start --service MyAzureSpringAppsInstance --resource-group MyResourceGroup +""" diff --git a/src/spring/azext_spring/_params.py b/src/spring/azext_spring/_params.py index 57467910ac8..9090f50eed0 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -1307,3 +1307,6 @@ 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 migration-aca start') as c: + c.argument('service', service_name_type) diff --git a/src/spring/azext_spring/commands.py b/src/spring/azext_spring/commands.py index 5d75c5c5a53..305b7ff83d8 100644 --- a/src/spring/azext_spring/commands.py +++ b/src/spring/azext_spring/commands.py @@ -519,5 +519,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 migration-aca', custom_command_type=spring_routing_util, + exception_handler=handle_asc_exception, is_preview=True) as g: + g.custom_command('start', 'spring_migration_aca_start') + with self.command_group('spring', exception_handler=handle_asc_exception): pass 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..0af170e484d --- /dev/null +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -0,0 +1,149 @@ +import os + +from knack.log import get_logger +from jinja2 import Environment, FileSystemLoader + +logger = get_logger(__name__) + + +def migration_aca_start(cmd, client, resource_group, service): + # API calls + print("Start API calls to get ASA service, apps and deployments...") + asa = get_asa(client, resource_group, service) + + # Extract necessary properties from asa_service to aca_env, asa_app to aca_app, asa_deployment to aca_revision + print("Start to convert ASA resources to ACA resources...") + aca = convert_asa_to_aca(asa) + + # Define the parameters for the Bicep template and output the Bicep files + print("Start to generate ACA bicep files based on parameters...") + # Prepare bicep parameters + main_bicep_params, env_bicep_params, apps_bicep_params = get_aca_bicep_params(aca) + + output_dir = "output" + template_dir = "templates" + script_dir = os.path.dirname(os.path.abspath(__file__)) + + env = Environment(loader=FileSystemLoader(os.path.join(script_dir, template_dir))) + + # Generate the Bicep files + print(env_bicep_params) + generate_bicep_file(env, "main.bicep.j2", os.path.join(output_dir, "main.bicep"), main_bicep_params) + generate_bicep_file(env, "environment.bicep.j2", os.path.join(output_dir, "environment.bicep"), env_bicep_params) + # [print(app) for app in apps_bicep_params] + [generate_bicep_file(env, "app.bicep.j2", os.path.join(output_dir, f"app-{app['containerAppName']}.bicep"), app) for app in apps_bicep_params] + print("Succeed to generate Bicep files") + + # Generate readme file + print("Start to generate the readme file based on parameters...") + readme_params = {} + generate_bicep_file(env, "readme_template.j2", os.path.join(output_dir, "readme"), readme_params) + print("Generated readme file.") + + +def get_asa(client, resource_group, service): + asa_service = client.services.get(resource_group, service) + asa_apps = client.apps.list(resource_group, service) + asa = { + "service": asa_service, + "apps": asa_apps, + } + return asa + + +def convert_asa_to_aca(asa): + aca_environment = _convert_asa_service_to_aca_environment(asa["service"]) + aca_apps = [_convert_asa_app_to_aca_app(app) for app in asa["apps"]] + # aca_revisions = [_convert_asa_deployment_to_aca_revision(deployment) for deployment in asa["deployments"]] + aca = { + "environment": aca_environment, + "apps": aca_apps, + # "revisions": aca_revisions + } + return aca + + +def get_aca_bicep_params(aca): + main_bicep_params = {} + env_bicep_params = _get_aca_environment_bicep_params(aca["environment"]) + apps_bicep_params = [_get_aca_app_bicep_params(app) for app in aca["apps"]] # array of container app parameters + return main_bicep_params, env_bicep_params, apps_bicep_params + + +def replace_variables_in_template(template_path, output_path, variables): + if not os.path.exists(template_path): + raise FileNotFoundError(f"Template file {template_path} not found") + + with open(template_path, 'r', encoding='utf-8') as template_file: + content = template_file.read() + + for key, value in variables.items(): + placeholder = f"{{{{{key}}}}}" + content = content.replace(placeholder, f"'{value}'") + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as output_file: + output_file.write(content) + + +def generate_bicep_file(env, template_name, output_path, variables): + template = env.get_template(template_name) + content = template.render(variables) + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as output_file: + output_file.write(content) + + +def _convert_asa_service_to_aca_environment(asa_service): + aca_environment = { + "name": asa_service.name, + "location": asa_service.location, + "log_analytics": f"log-{asa_service.name}", + } + return aca_environment + + +def _convert_asa_app_to_aca_app(asa_app): + aca_app = { + "name": asa_app.name, + "container_image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "target_port": 80, + "cpu_core": "0.5", + "memory_size": "1", + "min_replicas": 1, + "max_replicas": 5, + } + return aca_app + + +def _convert_asa_deployment_to_aca_revision(asa_deployment): + aca_revision = { + "name": asa_deployment.name, + "app": asa_deployment.app_name, + "cpu_core": "0.5", + "memory_size": "1", + "min_replicas": 1, + "max_replicas": 5, + } + return aca_revision + + +def _get_aca_environment_bicep_params(aca_environment): + return { + "containerAppEnvName": aca_environment["name"], + "location": aca_environment["location"], + "containerAppLogAnalyticsName": aca_environment["log_analytics"], + } + + +def _get_aca_app_bicep_params(aca_app): + return { + "containerAppName": aca_app["name"], + "containerImage": aca_app["container_image"], + "targetPort": aca_app["target_port"], + "cpuCore": aca_app["cpu_core"], + "memorySize": aca_app["memory_size"], + "minReplicas": aca_app["min_replicas"], + "maxReplicas": aca_app["max_replicas"], + } diff --git a/src/spring/azext_spring/migration/templates/app.bicep.j2 b/src/spring/azext_spring/migration/templates/app.bicep.j2 new file mode 100644 index 00000000000..1026dd1c4d2 --- /dev/null +++ b/src/spring/azext_spring/migration/templates/app.bicep.j2 @@ -0,0 +1,74 @@ +param containerAppName string = {{containerAppName}} +param containerImage string = {{containerImage}} +param targetPort int = {{targetPort}} + +param cpuCore string = {{cpuCore}} +param memorySize string = {{memorySize}} + +@description('Minimum number of replicas that will be deployed') +@minValue(0) +param minReplicas int = {{minReplicas}} + +@description('Maximum number of replicas that will be deployed') +@minValue(1) +param maxReplicas int = {{maxReplicas}} + +param containerAppEnvId string + +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: containerAppName + location: resourceGroup().location + properties: { + managedEnvironmentId: containerAppEnvId + configuration: { + ingress: { + external: true + targetPort: targetPort + allowInsecure: false + traffic: [ + { + latestRevision: true + weight: 100 + } + ] + } + } + template: { + containers: [ + { + name: containerAppName + image: containerImage + resources: { + cpu: json(cpuCore) + memory: '${memorySize}Gi' + } + } + ] + scale: { + minReplicas: minReplicas + maxReplicas: maxReplicas + } + } + } +} + +resource conainerAppRevision 'Microsoft.App/containerApp/revisions@2024-03-01' = [for revision in range(1, 2): { + name: '${containerAppName}-${revision}' + parent: containerApp + properties: { + template: { + containers: [ + { + name: containerAppName + image: containerImage + resources: { + cpu: json(cpuCore) + memory: '${memorySize}Gi' + } + } + ] + } + } +}] + +output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn diff --git a/src/spring/azext_spring/migration/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/templates/environment.bicep.j2 new file mode 100644 index 00000000000..8ed91e22278 --- /dev/null +++ b/src/spring/azext_spring/migration/templates/environment.bicep.j2 @@ -0,0 +1,53 @@ +param containerAppEnvName string = {{containerAppEnvName}} +param containerAppLogAnalyticsName string = {{containerAppLogAnalyticsName}} +param location string = {{location}} +param daprAIInstrumentationKey string? = {{daprAIInstrumentationKey}} +param daprAIConnectionString string? = {{daprAIConnectionString}} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: containerAppLogAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } +} + +resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: containerAppEnvName + location: location + properties: { + daprAIInstrumentationKey: daprAIInstrumentationKey + daprAIConnectionString: daprAIConnectionString + // vnetConfiguration: {} + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + zoneRedundant: '{{zoneRedundant}}' + // customDomainConfiguration: {} + workloadProfiles: [ + { + name: '{{workloadProfileName}}' + workloadProfiles: '{{workloadProfiles}}' + minimumCount: '{{minimumCount}}' + maximumCount: '{{maximumCount}}' + } + ] + // kedaConfiguration: {} + // daprConfiguration: {} + infrastructureResourceGroup: '{{infrastructureResourceGroup}}' + peerAuthentication: { + mtls: { + enabled: '{{mtlsEnabled}}' + } + } + //peerTrafficConfiguration: {} + } +} + +output containerAppEnvId string = containerAppEnv.id diff --git a/src/spring/azext_spring/migration/templates/main.bicep.j2 b/src/spring/azext_spring/migration/templates/main.bicep.j2 new file mode 100644 index 00000000000..4f117772fdd --- /dev/null +++ b/src/spring/azext_spring/migration/templates/main.bicep.j2 @@ -0,0 +1,39 @@ +// Env +param containerAppEnvName string +param containerAppLogAnalyticsName string +param location string +param apps array +param revisions object + +// App +param containerAppName string +param containerImage string +param targetPort int +param cpuCore string +param memorySize string +param minReplicas int +param maxReplicas int + +module containerAppEnv 'environment.bicep' = { + name: 'containerAppEnvDeployment' + params: { + containerAppEnvName: containerAppEnvName + containerAppLogAnalyticsName: containerAppLogAnalyticsName + location: location + } +} + +module appModule 'app.bicep' = [for app in apps: { + name: '${app.name}' + dependsOn: [containerAppEnv] + params: { + containerAppName: containerAppName + containerImage: containerImage + targetPort: targetPort + cpuCore: cpuCore + memorySize: memorySize + minReplicas: minReplicas + maxReplicas: maxReplicas + containerAppEnvId: containerAppEnv.outputs.containerAppEnvId + } +}] \ No newline at end of file diff --git a/src/spring/azext_spring/migration/templates/readme_template.j2 b/src/spring/azext_spring/migration/templates/readme_template.j2 new file mode 100644 index 00000000000..173da9b46cb --- /dev/null +++ b/src/spring/azext_spring/migration/templates/readme_template.j2 @@ -0,0 +1,52 @@ +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. Additionally, some configuration updates must be made manually by referencing public Azure documentation after deploying the Bicep files. + +## Prerequisites + +Before you begin, ensure you have the following: + +1. Azure Subscription: An active Azure subscription. + +2. Azure CLI: Installed and authenticated. You can download it here. + +3. Bicep CLI: Installed and available. Installation steps are available here. + +4. Permissions: Sufficient permissions to create and manage resources in your Azure subscription. + +## Steps to Deploy Azure Container Apps Using Bicep Files + +1. Review and Customize Bicep Files + +Open the Bicep files generated in the directory and review the parameters and resource definitions. Modify parameter values as needed to align with your environment (e.g., resource group name, location, application names). + +2. [Optional] Create a new Resource Group + +If the resource group specified in the Bicep files does not already exist, create one: + +az group create --name --location + +3. Deploy the Bicep Files + +Deploy the Bicep files using the Azure CLI: + +az deployment group create \ + --resource-group \ + --template-file \ + --parameters .json + +Replace .json with the path to your parameter file. + +5. Validate Deployment + +After deployment, validate that all resources, including the Azure Container Apps environment and applications, are correctly provisioned. You can check this in the Azure Portal or using the Azure CLI: + +az containerapp list --resource-group + +## Post-Deployment Configuration + +Some properties and configurations are not included in the Bicep files and must be manually updated. Refer to the official Azure documentation for detailed steps: + +### Networking + +### Scaling Settings: Configure scaling policies following Configure Scaling. + +## Known Limitations \ No newline at end of file diff --git a/src/spring/azext_spring/spring_instance.py b/src/spring/azext_spring/spring_instance.py index 7e3b558ef60..fbb460bb659 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,7 @@ 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_aca_start(cmd, client, resource_group, service): + migration_aca_start(cmd, client, resource_group, service) From 88e1da5441e93b7099aece84a3855f20e2f85793 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Sun, 12 Jan 2025 16:30:33 +0800 Subject: [PATCH 02/98] add method to export arm template of asa --- src/spring/azext_spring/commands.py | 3 ++- .../migration/migration_operations.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/spring/azext_spring/commands.py b/src/spring/azext_spring/commands.py index 305b7ff83d8..2d947f7601c 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,7 +520,7 @@ 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 migration-aca', custom_command_type=spring_routing_util, + with self.command_group('spring migration-aca', 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('start', 'spring_migration_aca_start') diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 0af170e484d..d76081f3130 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -1,5 +1,8 @@ import os +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 jinja2 import Environment, FileSystemLoader @@ -10,6 +13,7 @@ def migration_aca_start(cmd, client, resource_group, service): # API calls print("Start API calls to get ASA service, apps and deployments...") asa = get_asa(client, resource_group, service) + asa_arm = export_asa_arm_template(cmd, resource_group, service) # Extract necessary properties from asa_service to aca_env, asa_app to aca_app, asa_deployment to aca_revision print("Start to convert ASA resources to ACA resources...") @@ -50,6 +54,28 @@ def get_asa(client, resource_group, service): } return asa +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 = None + + 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 def convert_asa_to_aca(asa): aca_environment = _convert_asa_service_to_aca_environment(asa["service"]) From 35291061e5f5e156d76a94841ffa17f2e1df0b2e Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 13 Jan 2025 19:48:15 +0800 Subject: [PATCH 03/98] Add param file --- .../azext_spring/migration/templates/environment.bicep.j2 | 6 ++++-- src/spring/azext_spring/migration/templates/main.bicep.j2 | 4 ++++ .../azext_spring/migration/templates/param.bicepparam.j2 | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/spring/azext_spring/migration/templates/param.bicepparam.j2 diff --git a/src/spring/azext_spring/migration/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/templates/environment.bicep.j2 index 8ed91e22278..e2c55d2db0a 100644 --- a/src/spring/azext_spring/migration/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/templates/environment.bicep.j2 @@ -1,6 +1,8 @@ param containerAppEnvName string = {{containerAppEnvName}} param containerAppLogAnalyticsName string = {{containerAppLogAnalyticsName}} param location string = {{location}} +param workloadProfileName string +param workloadProfileType string param daprAIInstrumentationKey string? = {{daprAIInstrumentationKey}} param daprAIConnectionString string? = {{daprAIConnectionString}} @@ -32,8 +34,8 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { // customDomainConfiguration: {} workloadProfiles: [ { - name: '{{workloadProfileName}}' - workloadProfiles: '{{workloadProfiles}}' + name: workloadProfileName + workloadProfileType: workloadProfileType minimumCount: '{{minimumCount}}' maximumCount: '{{maximumCount}}' } diff --git a/src/spring/azext_spring/migration/templates/main.bicep.j2 b/src/spring/azext_spring/migration/templates/main.bicep.j2 index 4f117772fdd..72a133a5b52 100644 --- a/src/spring/azext_spring/migration/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/templates/main.bicep.j2 @@ -4,6 +4,8 @@ param containerAppLogAnalyticsName string param location string param apps array param revisions object +param workloadProfileName string +param workloadProfileType string // App param containerAppName string @@ -20,6 +22,8 @@ module containerAppEnv 'environment.bicep' = { containerAppEnvName: containerAppEnvName containerAppLogAnalyticsName: containerAppLogAnalyticsName location: location + workloadProfileName: workloadProfileName + workloadProfileType: workloadProfileType } } diff --git a/src/spring/azext_spring/migration/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/templates/param.bicepparam.j2 new file mode 100644 index 00000000000..62c211dceb9 --- /dev/null +++ b/src/spring/azext_spring/migration/templates/param.bicepparam.j2 @@ -0,0 +1,4 @@ +using './main.bicep' + +param workloadProfileName = 'Consumption' +param workloadProfileType = 'Consumption' \ No newline at end of file From c40ed7e2ced9de7afadc4388b3dab3cdb092c6a5 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 16 Jan 2025 14:03:41 +0800 Subject: [PATCH 04/98] add options param --- src/spring/azext_spring/migration/migration_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index d76081f3130..d884732bf1a 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -61,7 +61,7 @@ def export_asa_arm_template(cmd, resource_group, service): subscription, resource_group, service) logger.info("service_resource_id: '%s'", service_resource_id) resources.append(service_resource_id) - options = None + options = "SkipAllParameterization,IncludeParameterDefaultValue" ExportTemplateRequest = cmd.get_models('ExportTemplateRequest') export_template_request = ExportTemplateRequest(resources=resources, options=options) From ed21f2191f7ad92530a4003320c702f84558791f Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Sat, 18 Jan 2025 15:11:30 +0800 Subject: [PATCH 05/98] refactor --- .../migration/converter/app_converter.py | 28 ++++ .../migration/converter/base_converter.py | 82 +++++++++++ .../converter/environment_converter.py | 20 +++ .../migration/converter/readme_converter.py | 12 ++ .../migration/converter/revision_converter.py | 19 +++ .../migration/migration_operations.py | 129 +++--------------- 6 files changed, 178 insertions(+), 112 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/app_converter.py create mode 100644 src/spring/azext_spring/migration/converter/base_converter.py create mode 100644 src/spring/azext_spring/migration/converter/environment_converter.py create mode 100644 src/spring/azext_spring/migration/converter/readme_converter.py create mode 100644 src/spring/azext_spring/migration/converter/revision_converter.py 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..3b54325dc48 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -0,0 +1,28 @@ +from converter import ConverterTemplate + +# Concrete Converter Subclass for Container App +class AppConverter(ConverterTemplate): + def load_source(self, source): + self.source = source['properties'] + + def calculate_data(self): + self.data = { + "name": self.source.name, + "container_image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "target_port": 80, + "cpu_core": "0.5", + "memory_size": "1", + "min_replicas": 1, + "max_replicas": 5, + + "containerAppName": self.source.["name"], + "containerImage": self.source.["container_image"], + "targetPort": self.source.["target_port"], + "cpuCore": self.source.["cpu_core"], + "memorySize": self.source.["memory_size"], + "minReplicas": self.source.["min_replicas"], + "maxReplicas": self.source.["max_replicas"], + } + + def get_template_name(self): + return "app.bicep" \ No newline at end of file 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..f2d9d2f03ea --- /dev/null +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -0,0 +1,82 @@ +import os + +from converter import converter, abstractmethod +from jinja2 import Template +from converter import EnvironmentConverter, AppConverter, RevisionConverter, ReadMeConverter + +# Abstract Base Class for Converter +class ConverterTemplate(converter): + def __init__(self): + self.params = {} # custom facing parameters for the converter + self.data = {} # output data of the converter + self.source = {} # input data of the converter + + def set_params(self, params): + self.params = params + + def convert(self, source): + self.load_source(source) + self.calculate_data() + return self.generate_output() + + @abstractmethod + def load_source(self, source): # load the input data + pass + + @abstractmethod + def calculate_data(self): # calculate the output data + pass + + @abstractmethod + def get_template_name(self): + pass + + def generate_output(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + template_name = self.get_template_name() + with open(f"{script_dir}/templates/{template_name}.j2") as file: + template = Template(file.read()) + return template.render(data=self.data, params=self.params) + +# Context Class +class ConversionContext: + def __init__(self): + self.converters = [] + + def add_converter(self, converter: ConverterTemplate): + self.converters.append(converter) + + def get_converter(self, converter_type: type): + for converter in self.converters: + if isinstance(converter, converter_type): + return converter + raise ValueError(f"Unknown converter type: {converter_type}") + + def set_params_for_converter(self, converter_type, params): + for converter in self.converters: + if isinstance(converter, converter_type): + converter.set_params(params) + + def run_converters(self, source): + converted_contents = [] + for resource in source['resources']: + if resource['type'] == 'Microsoft.AppPlatform/Spring': + converted_contents.append(self.get_converter(EnvironmentConverter).convert(resource)) + if resource['type'] == 'Microsoft.AppPlatform/apps': + converted_contents.append(self.get_converter(AppConverter).convert(resource)) + if resource['type'] == 'Microsoft.AppPlatform/Spring/buildServices': + pass + if resource['type'] == 'Microsoft.AppPlatform/Spring/apps/deployments': + converted_contents.append(self.get_converter(RevisionConverter).convert(resource)) + if resource['type'] == 'Microsoft.AppPlatform/Spring/configServers': + pass + converted_contents.append(self.get_converter(ReadMeConverter).convert(resource)) + return converted_contents + + def save_to_files(self, converted_contents, output_path): + os.makedirs(os.path.dirname(output_path), exist_ok=True) + for i, content in enumerate(converted_contents): + filename = f"{output_path}/export_script_{i+1}.bicep" + with open(filename, 'w', encoding='utf-8') as output_file: + print("Start to generate the {filename} file ...") + 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..75dc1f3edfd --- /dev/null +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -0,0 +1,20 @@ +from converter import ConverterTemplate + +# Concrete Subclass for Container App Environment +class EnvironmentConverter(ConverterTemplate): + def load_source(self, source): + self.source = source['properties'] + + def calculate_data(self): + self.data = { + "name": self.source.name, + "location": self.source.location, + "log_analytics": f"log-{self.source.name}", + + "containerAppEnvName": self.source["name"], + "location": self.source["location"], + "containerAppLogAnalyticsName": self.source["log_analytics"], + } + + def get_template_name(self): + return "environment.bicep" \ No newline at end of file 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..19647ec6652 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -0,0 +1,12 @@ +from converter import ConverterTemplate + +# Concrete Converter Subclass for Read Me +class ReadMeConverter(ConverterTemplate): + def load_source(self, source): + pass + + def calculate_data(self): + pass + + def get_template_name(self): + return "readme_template" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/revision_converter.py b/src/spring/azext_spring/migration/converter/revision_converter.py new file mode 100644 index 00000000000..707b1d47c93 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/revision_converter.py @@ -0,0 +1,19 @@ +from converter import ConverterTemplate + +# Concrete Converter Subclass for Revision +class RevisionConverter(ConverterTemplate): + def load_source(self, source): + self.source = source['properties'] + + def calculate_data(self): + self.data = { + "name": self.source.name, + "app": self.source.app_name, + "cpu_core": "0.5", + "memory_size": "1", + "min_replicas": 1, + "max_replicas": 5, + } + + def get_template_name(self): + return "revision.bicep" \ 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 index d884732bf1a..feae8a79346 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -5,6 +5,7 @@ from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from jinja2 import Environment, FileSystemLoader +from converter import ConversionContext, EnvironmentConverter, AppConverter, RevisionConverter, ReadMeConverter logger = get_logger(__name__) @@ -12,48 +13,31 @@ def migration_aca_start(cmd, client, resource_group, service): # API calls print("Start API calls to get ASA service, apps and deployments...") - asa = get_asa(client, resource_group, service) asa_arm = export_asa_arm_template(cmd, resource_group, service) - # Extract necessary properties from asa_service to aca_env, asa_app to aca_app, asa_deployment to aca_revision - print("Start to convert ASA resources to ACA resources...") - aca = convert_asa_to_aca(asa) + # Create context and add converters + context = ConversionContext() + context.add_converter(EnvironmentConverter()) + context.add_converter(AppConverter()) + context.add_converter(RevisionConverter()) + context.add_converter(ReadMeConverter()) # Define the parameters for the Bicep template and output the Bicep files print("Start to generate ACA bicep files based on parameters...") + # Prepare bicep parameters - main_bicep_params, env_bicep_params, apps_bicep_params = get_aca_bicep_params(aca) + main_bicep_params = get_aca_bicep_params(asa_arm) - output_dir = "output" - template_dir = "templates" - script_dir = os.path.dirname(os.path.abspath(__file__)) + # Set parameters for EnvironmentConverter such as workload profile + context.set_params_for_converter(EnvironmentConverter, main_bicep_params) - env = Environment(loader=FileSystemLoader(os.path.join(script_dir, template_dir))) + # Run all converters + converted_contents = context.run_converters(asa_arm) - # Generate the Bicep files - print(env_bicep_params) - generate_bicep_file(env, "main.bicep.j2", os.path.join(output_dir, "main.bicep"), main_bicep_params) - generate_bicep_file(env, "environment.bicep.j2", os.path.join(output_dir, "environment.bicep"), env_bicep_params) - # [print(app) for app in apps_bicep_params] - [generate_bicep_file(env, "app.bicep.j2", os.path.join(output_dir, f"app-{app['containerAppName']}.bicep"), app) for app in apps_bicep_params] + # Save each line of converted content to a separate file + context.save_to_files(converted_contents, 'output') print("Succeed to generate Bicep files") - # Generate readme file - print("Start to generate the readme file based on parameters...") - readme_params = {} - generate_bicep_file(env, "readme_template.j2", os.path.join(output_dir, "readme"), readme_params) - print("Generated readme file.") - - -def get_asa(client, resource_group, service): - asa_service = client.services.get(resource_group, service) - asa_apps = client.apps.list(resource_group, service) - asa = { - "service": asa_service, - "apps": asa_apps, - } - return asa - def export_asa_arm_template(cmd, resource_group, service): resources = [] subscription = get_subscription_id(cmd.cli_ctx) @@ -77,24 +61,8 @@ def export_asa_arm_template(cmd, resource_group, service): parameters=export_template_request) return result.template -def convert_asa_to_aca(asa): - aca_environment = _convert_asa_service_to_aca_environment(asa["service"]) - aca_apps = [_convert_asa_app_to_aca_app(app) for app in asa["apps"]] - # aca_revisions = [_convert_asa_deployment_to_aca_revision(deployment) for deployment in asa["deployments"]] - aca = { - "environment": aca_environment, - "apps": aca_apps, - # "revisions": aca_revisions - } - return aca - - -def get_aca_bicep_params(aca): - main_bicep_params = {} - env_bicep_params = _get_aca_environment_bicep_params(aca["environment"]) - apps_bicep_params = [_get_aca_app_bicep_params(app) for app in aca["apps"]] # array of container app parameters - return main_bicep_params, env_bicep_params, apps_bicep_params - +def get_aca_bicep_params(aca_arm): + return {"key1": "value1"} def replace_variables_in_template(template_path, output_path, variables): if not os.path.exists(template_path): @@ -110,66 +78,3 @@ def replace_variables_in_template(template_path, output_path, variables): os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, 'w', encoding='utf-8') as output_file: output_file.write(content) - - -def generate_bicep_file(env, template_name, output_path, variables): - template = env.get_template(template_name) - content = template.render(variables) - - os.makedirs(os.path.dirname(output_path), exist_ok=True) - with open(output_path, 'w', encoding='utf-8') as output_file: - output_file.write(content) - - -def _convert_asa_service_to_aca_environment(asa_service): - aca_environment = { - "name": asa_service.name, - "location": asa_service.location, - "log_analytics": f"log-{asa_service.name}", - } - return aca_environment - - -def _convert_asa_app_to_aca_app(asa_app): - aca_app = { - "name": asa_app.name, - "container_image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", - "target_port": 80, - "cpu_core": "0.5", - "memory_size": "1", - "min_replicas": 1, - "max_replicas": 5, - } - return aca_app - - -def _convert_asa_deployment_to_aca_revision(asa_deployment): - aca_revision = { - "name": asa_deployment.name, - "app": asa_deployment.app_name, - "cpu_core": "0.5", - "memory_size": "1", - "min_replicas": 1, - "max_replicas": 5, - } - return aca_revision - - -def _get_aca_environment_bicep_params(aca_environment): - return { - "containerAppEnvName": aca_environment["name"], - "location": aca_environment["location"], - "containerAppLogAnalyticsName": aca_environment["log_analytics"], - } - - -def _get_aca_app_bicep_params(aca_app): - return { - "containerAppName": aca_app["name"], - "containerImage": aca_app["container_image"], - "targetPort": aca_app["target_port"], - "cpuCore": aca_app["cpu_core"], - "memorySize": aca_app["memory_size"], - "minReplicas": aca_app["min_replicas"], - "maxReplicas": aca_app["max_replicas"], - } From 2e5a05e383a4ab97ed4627ce0d165753ac17946c Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Sun, 19 Jan 2025 01:22:25 +0800 Subject: [PATCH 06/98] refactor --- .../migration/converter/__init__.py | 4 ++ .../migration/converter/app_converter.py | 34 +++++----- .../migration/converter/base_converter.py | 48 +------------- .../migration/converter/conversion_context.py | 65 +++++++++++++++++++ .../converter/environment_converter.py | 10 +-- .../migration/converter/gateway_converter.py | 16 +++++ .../migration/converter/readme_converter.py | 2 +- .../migration/converter/revision_converter.py | 24 ++++--- .../migration/migration_operations.py | 8 ++- .../migration/templates/app.bicep.j2 | 34 +++++----- .../migration/templates/environment.bicep.j2 | 11 ++++ .../migration/templates/main.bicep.j2 | 43 ------------ .../migration/templates/revision.bicep.j2 | 28 ++++++++ 13 files changed, 181 insertions(+), 146 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/__init__.py create mode 100644 src/spring/azext_spring/migration/converter/conversion_context.py create mode 100644 src/spring/azext_spring/migration/converter/gateway_converter.py delete mode 100644 src/spring/azext_spring/migration/templates/main.bicep.j2 create mode 100644 src/spring/azext_spring/migration/templates/revision.bicep.j2 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/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 3b54325dc48..d40576fb759 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -1,28 +1,24 @@ -from converter import ConverterTemplate +from .base_converter import ConverterTemplate # Concrete Converter Subclass for Container App class AppConverter(ConverterTemplate): def load_source(self, source): - self.source = source['properties'] + self.source = [] + for resource in source: + self.source.append(resource) def calculate_data(self): - self.data = { - "name": self.source.name, - "container_image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", - "target_port": 80, - "cpu_core": "0.5", - "memory_size": "1", - "min_replicas": 1, - "max_replicas": 5, - - "containerAppName": self.source.["name"], - "containerImage": self.source.["container_image"], - "targetPort": self.source.["target_port"], - "cpuCore": self.source.["cpu_core"], - "memorySize": self.source.["memory_size"], - "minReplicas": self.source.["min_replicas"], - "maxReplicas": self.source.["max_replicas"], - } + self.data.apps = [] + for app in self.source: + self.data.apps.append({ + "containerAppName": app["properties"]["name"], + "containerImage": app["container_image"], + "targetPort": app["target_port"], + "cpuCore": app["cpu_core"], + "memorySize": app["memory_size"], + "minReplicas": app["min_replicas"], + "maxReplicas": app["max_replicas"], + }) def get_template_name(self): return "app.bicep" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index f2d9d2f03ea..a80a78adc31 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -1,11 +1,10 @@ import os -from converter import converter, abstractmethod +from abc import ABC, abstractmethod from jinja2 import Template -from converter import EnvironmentConverter, AppConverter, RevisionConverter, ReadMeConverter # Abstract Base Class for Converter -class ConverterTemplate(converter): +class ConverterTemplate(ABC): def __init__(self): self.params = {} # custom facing parameters for the converter self.data = {} # output data of the converter @@ -37,46 +36,3 @@ def generate_output(self): with open(f"{script_dir}/templates/{template_name}.j2") as file: template = Template(file.read()) return template.render(data=self.data, params=self.params) - -# Context Class -class ConversionContext: - def __init__(self): - self.converters = [] - - def add_converter(self, converter: ConverterTemplate): - self.converters.append(converter) - - def get_converter(self, converter_type: type): - for converter in self.converters: - if isinstance(converter, converter_type): - return converter - raise ValueError(f"Unknown converter type: {converter_type}") - - def set_params_for_converter(self, converter_type, params): - for converter in self.converters: - if isinstance(converter, converter_type): - converter.set_params(params) - - def run_converters(self, source): - converted_contents = [] - for resource in source['resources']: - if resource['type'] == 'Microsoft.AppPlatform/Spring': - converted_contents.append(self.get_converter(EnvironmentConverter).convert(resource)) - if resource['type'] == 'Microsoft.AppPlatform/apps': - converted_contents.append(self.get_converter(AppConverter).convert(resource)) - if resource['type'] == 'Microsoft.AppPlatform/Spring/buildServices': - pass - if resource['type'] == 'Microsoft.AppPlatform/Spring/apps/deployments': - converted_contents.append(self.get_converter(RevisionConverter).convert(resource)) - if resource['type'] == 'Microsoft.AppPlatform/Spring/configServers': - pass - converted_contents.append(self.get_converter(ReadMeConverter).convert(resource)) - return converted_contents - - def save_to_files(self, converted_contents, output_path): - os.makedirs(os.path.dirname(output_path), exist_ok=True) - for i, content in enumerate(converted_contents): - filename = f"{output_path}/export_script_{i+1}.bicep" - with open(filename, 'w', encoding='utf-8') as output_file: - print("Start to generate the {filename} file ...") - output_file.write(content) 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..3e68aa76fd2 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -0,0 +1,65 @@ +import os + +from abc import ABC, abstractmethod +from jinja2 import Template +from .base_converter import ConverterTemplate +from .environment_converter import EnvironmentConverter +from .app_converter import AppConverter +from .revision_converter import RevisionConverter +from .readme_converter import ReadMeConverter + +# Context Class +class ConversionContext: + def __init__(self): + self.converters = [] + + def add_converter(self, converter: ConverterTemplate): + self.converters.append(converter) + + def get_converter(self, converter_type: type): + for converter in self.converters: + if isinstance(converter, converter_type): + return converter + raise ValueError(f"Unknown converter type: {converter_type}") + + def set_params_for_converter(self, converter_type, params): + for converter in self.converters: + if isinstance(converter, converter_type): + converter.set_params(params) + + def run_converters(self, source): + converted_contents = [] + source_wrapper = SourceDataWrapper(source) + + converted_contents.append( + self.get_converter(EnvironmentConverter).convert( + source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + ) + ) + converted_contents.append( + self.get_converter(AppConverter).convert( + source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps') + ) + ) + converted_contents.append( + self.get_converter(RevisionConverter).convert( + source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps/deployments') + ) + ) + converted_contents.append(self.get_converter(ReadMeConverter).convert(None)) + return converted_contents + + def save_to_files(self, converted_contents, output_path): + os.makedirs(os.path.dirname(output_path), exist_ok=True) + for i, content in enumerate(converted_contents): + filename = f"{output_path}/export_script_{i+1}.bicep" + with open(filename, 'w', encoding='utf-8') as output_file: + print("Start to generate the {filename} file ...") + output_file.write(content) + +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] \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 75dc1f3edfd..fd51576cae9 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -1,4 +1,4 @@ -from converter import ConverterTemplate +from .base_converter import ConverterTemplate # Concrete Subclass for Container App Environment class EnvironmentConverter(ConverterTemplate): @@ -7,13 +7,9 @@ def load_source(self, source): def calculate_data(self): self.data = { - "name": self.source.name, + "containerAppEnvName": self.source.name, "location": self.source.location, - "log_analytics": f"log-{self.source.name}", - - "containerAppEnvName": self.source["name"], - "location": self.source["location"], - "containerAppLogAnalyticsName": self.source["log_analytics"], + "containerAppLogAnalyticsName": f"log-{self.source.name}", } def get_template_name(self): 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..062646b94e2 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -0,0 +1,16 @@ +from .base_converter import ConverterTemplate + +# Concrete Converter Subclass for Gateway +class GatewayConverter(ConverterTemplate): + def __init__(self, client): + self.client = client + + def load_source(self, source): + # Call the client to get additional data + pass + + def calculate_data(self): + pass + + def get_template_name(self): + return "gateway.bicep" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 19647ec6652..484cabc2a09 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -1,4 +1,4 @@ -from converter import ConverterTemplate +from .base_converter import ConverterTemplate # Concrete Converter Subclass for Read Me class ReadMeConverter(ConverterTemplate): diff --git a/src/spring/azext_spring/migration/converter/revision_converter.py b/src/spring/azext_spring/migration/converter/revision_converter.py index 707b1d47c93..21c692315a2 100644 --- a/src/spring/azext_spring/migration/converter/revision_converter.py +++ b/src/spring/azext_spring/migration/converter/revision_converter.py @@ -1,19 +1,23 @@ -from converter import ConverterTemplate +from .base_converter import ConverterTemplate # Concrete Converter Subclass for Revision class RevisionConverter(ConverterTemplate): def load_source(self, source): - self.source = source['properties'] + self.source = [] + for resource in source: + self.source.append(resource) def calculate_data(self): - self.data = { - "name": self.source.name, - "app": self.source.app_name, - "cpu_core": "0.5", - "memory_size": "1", - "min_replicas": 1, - "max_replicas": 5, - } + self.data.revisions = [] + for revision in self.source: + self.data.revisions.append({ + "name": revision["properties"]["name"], + "app": self.source.app_name, + "cpu_core": "0.5", + "memory_size": "1", + "min_replicas": 1, + "max_replicas": 5, + }) def get_template_name(self): return "revision.bicep" \ 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 index feae8a79346..4a7ca77e107 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -5,7 +5,12 @@ from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from jinja2 import Environment, FileSystemLoader -from converter import ConversionContext, EnvironmentConverter, AppConverter, RevisionConverter, ReadMeConverter +from .converter.conversion_context import ConversionContext +from .converter.environment_converter import EnvironmentConverter +from .converter.app_converter import AppConverter +from .converter.revision_converter import RevisionConverter +from .converter.gateway_converter import GatewayConverter +from .converter.readme_converter import ReadMeConverter logger = get_logger(__name__) @@ -20,6 +25,7 @@ def migration_aca_start(cmd, client, resource_group, service): context.add_converter(EnvironmentConverter()) context.add_converter(AppConverter()) context.add_converter(RevisionConverter()) + context.add_converter(GatewayConverter(client)) context.add_converter(ReadMeConverter()) # Define the parameters for the Bicep template and output the Bicep files diff --git a/src/spring/azext_spring/migration/templates/app.bicep.j2 b/src/spring/azext_spring/migration/templates/app.bicep.j2 index 1026dd1c4d2..8a5c588500f 100644 --- a/src/spring/azext_spring/migration/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/templates/app.bicep.j2 @@ -15,6 +15,21 @@ param maxReplicas int = {{maxReplicas}} param containerAppEnvId string +module appModule 'app.bicep' = [for app in data.apps: { + name: '${app.name}' + dependsOn: [containerAppEnv] + params: { + containerAppName: containerAppName + containerImage: containerImage + targetPort: targetPort + cpuCore: cpuCore + memorySize: memorySize + minReplicas: minReplicas + maxReplicas: maxReplicas + containerAppEnvId: containerAppEnv.outputs.containerAppEnvId + } +}] + resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { name: containerAppName location: resourceGroup().location @@ -52,23 +67,4 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { } } -resource conainerAppRevision 'Microsoft.App/containerApp/revisions@2024-03-01' = [for revision in range(1, 2): { - name: '${containerAppName}-${revision}' - parent: containerApp - properties: { - template: { - containers: [ - { - name: containerAppName - image: containerImage - resources: { - cpu: json(cpuCore) - memory: '${memorySize}Gi' - } - } - ] - } - } -}] - output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn diff --git a/src/spring/azext_spring/migration/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/templates/environment.bicep.j2 index e2c55d2db0a..b7fae802e19 100644 --- a/src/spring/azext_spring/migration/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/templates/environment.bicep.j2 @@ -6,6 +6,17 @@ param workloadProfileType string param daprAIInstrumentationKey string? = {{daprAIInstrumentationKey}} param daprAIConnectionString string? = {{daprAIConnectionString}} +module containerAppEnv 'environment.bicep' = { + name: 'containerAppEnvDeployment' + params: { + containerAppEnvName: containerAppEnvName + containerAppLogAnalyticsName: containerAppLogAnalyticsName + location: location + workloadProfileName: workloadProfileName + workloadProfileType: workloadProfileType + } +} + resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: containerAppLogAnalyticsName location: location diff --git a/src/spring/azext_spring/migration/templates/main.bicep.j2 b/src/spring/azext_spring/migration/templates/main.bicep.j2 deleted file mode 100644 index 72a133a5b52..00000000000 --- a/src/spring/azext_spring/migration/templates/main.bicep.j2 +++ /dev/null @@ -1,43 +0,0 @@ -// Env -param containerAppEnvName string -param containerAppLogAnalyticsName string -param location string -param apps array -param revisions object -param workloadProfileName string -param workloadProfileType string - -// App -param containerAppName string -param containerImage string -param targetPort int -param cpuCore string -param memorySize string -param minReplicas int -param maxReplicas int - -module containerAppEnv 'environment.bicep' = { - name: 'containerAppEnvDeployment' - params: { - containerAppEnvName: containerAppEnvName - containerAppLogAnalyticsName: containerAppLogAnalyticsName - location: location - workloadProfileName: workloadProfileName - workloadProfileType: workloadProfileType - } -} - -module appModule 'app.bicep' = [for app in apps: { - name: '${app.name}' - dependsOn: [containerAppEnv] - params: { - containerAppName: containerAppName - containerImage: containerImage - targetPort: targetPort - cpuCore: cpuCore - memorySize: memorySize - minReplicas: minReplicas - maxReplicas: maxReplicas - containerAppEnvId: containerAppEnv.outputs.containerAppEnvId - } -}] \ No newline at end of file diff --git a/src/spring/azext_spring/migration/templates/revision.bicep.j2 b/src/spring/azext_spring/migration/templates/revision.bicep.j2 new file mode 100644 index 00000000000..18a8adb943f --- /dev/null +++ b/src/spring/azext_spring/migration/templates/revision.bicep.j2 @@ -0,0 +1,28 @@ +param containerAppName string = {{containerAppName}} +param containerImage string = {{containerImage}} + +param cpuCore string = {{cpuCore}} +param memorySize string = {{memorySize}} + +param containerAppEnvId string + +resource conainerAppRevision 'Microsoft.App/containerApp/revisions@2024-03-01' = [for revision in range(1, 2): { + name: '${containerAppName}-${revision}' + parent: containerApp + properties: { + template: { + containers: [ + { + name: containerAppName + image: containerImage + resources: { + cpu: json(cpuCore) + memory: '${memorySize}Gi' + } + } + ] + } + } +}] + +output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn From 6cd8e63d8727441035485adc7a8ce6471ee1ae4e Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Mon, 20 Jan 2025 16:46:51 +0800 Subject: [PATCH 07/98] refactor --- .../migration/converter/app_converter.py | 8 +++---- .../migration/converter/base_converter.py | 6 +++++ .../migration/converter/conversion_context.py | 24 ++++++++++--------- .../converter/environment_converter.py | 8 +++---- .../migration/converter/main_converter.py | 12 ++++++++++ .../{ => converter}/templates/app.bicep.j2 | 15 ------------ .../templates/environment.bicep.j2 | 11 --------- .../converter/templates/main.bicep.j2 | 24 +++++++++++++++++++ .../templates/param.bicepparam.j2 | 0 .../templates/readme_template.j2 | 0 .../templates/revision.bicep.j2 | 2 -- .../migration/migration_operations.py | 17 +------------ 12 files changed, 64 insertions(+), 63 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/main_converter.py rename src/spring/azext_spring/migration/{ => converter}/templates/app.bicep.j2 (76%) rename src/spring/azext_spring/migration/{ => converter}/templates/environment.bicep.j2 (83%) create mode 100644 src/spring/azext_spring/migration/converter/templates/main.bicep.j2 rename src/spring/azext_spring/migration/{ => converter}/templates/param.bicepparam.j2 (100%) rename src/spring/azext_spring/migration/{ => converter}/templates/readme_template.j2 (100%) rename src/spring/azext_spring/migration/{ => converter}/templates/revision.bicep.j2 (88%) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index d40576fb759..4b9032b9303 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -11,10 +11,10 @@ def calculate_data(self): self.data.apps = [] for app in self.source: self.data.apps.append({ - "containerAppName": app["properties"]["name"], - "containerImage": app["container_image"], - "targetPort": app["target_port"], - "cpuCore": app["cpu_core"], + "containerAppName": app["name"], + "containerImage": self.params["container_image"], + "targetPort": self.params["target_port"], + "cpuCore": app['properties']["cpu_core"], "memorySize": app["memory_size"], "minReplicas": app["min_replicas"], "maxReplicas": app["max_replicas"], diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index a80a78adc31..ef21b63f827 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -4,6 +4,12 @@ from jinja2 import Template # 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 input data into the output data +# The conversion process is divided into three steps: +# 1. Load the input data +# 2. Calculate the output data +# 3. Generate the output data class ConverterTemplate(ABC): def __init__(self): self.params = {} # custom facing parameters for the converter diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 3e68aa76fd2..0013105ab82 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -7,6 +7,7 @@ from .app_converter import AppConverter from .revision_converter import RevisionConverter from .readme_converter import ReadMeConverter +from .main_converter import MainConverter # Context Class class ConversionContext: @@ -30,26 +31,27 @@ def set_params_for_converter(self, converter_type, params): def run_converters(self, source): converted_contents = [] source_wrapper = SourceDataWrapper(source) - + # converted_contents.append(self.get_converter(MainConverter).convert(None)) converted_contents.append( self.get_converter(EnvironmentConverter).convert( source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] ) ) - converted_contents.append( - self.get_converter(AppConverter).convert( - source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps') - ) - ) - converted_contents.append( - self.get_converter(RevisionConverter).convert( - source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps/deployments') - ) - ) + # converted_contents.append( + # self.get_converter(AppConverter).convert( + # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps') + # ) + # ) + # converted_contents.append( + # self.get_converter(RevisionConverter).convert( + # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps/deployments') + # ) + # ) converted_contents.append(self.get_converter(ReadMeConverter).convert(None)) return converted_contents def save_to_files(self, converted_contents, output_path): + print("Start to save the converted content to files ...") os.makedirs(os.path.dirname(output_path), exist_ok=True) for i, content in enumerate(converted_contents): filename = f"{output_path}/export_script_{i+1}.bicep" diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index fd51576cae9..f72ec44d219 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -3,13 +3,13 @@ # Concrete Subclass for Container App Environment class EnvironmentConverter(ConverterTemplate): def load_source(self, source): - self.source = source['properties'] + self.source = source def calculate_data(self): self.data = { - "containerAppEnvName": self.source.name, - "location": self.source.location, - "containerAppLogAnalyticsName": f"log-{self.source.name}", + "containerAppEnvName": self.source['name'], + "location": self.source['location'], + "containerAppLogAnalyticsName": f"log-{self.source['name']}", } def get_template_name(self): 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..d96edbfcee8 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -0,0 +1,12 @@ +from .base_converter import ConverterTemplate + +# Concrete Converter Subclass for Read Me +class MainConverter(ConverterTemplate): + def load_source(self, source): + pass + + def calculate_data(self): + pass + + def get_template_name(self): + return "main.bicep" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 similarity index 76% rename from src/spring/azext_spring/migration/templates/app.bicep.j2 rename to src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index 8a5c588500f..8abcdbbff85 100644 --- a/src/spring/azext_spring/migration/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -15,21 +15,6 @@ param maxReplicas int = {{maxReplicas}} param containerAppEnvId string -module appModule 'app.bicep' = [for app in data.apps: { - name: '${app.name}' - dependsOn: [containerAppEnv] - params: { - containerAppName: containerAppName - containerImage: containerImage - targetPort: targetPort - cpuCore: cpuCore - memorySize: memorySize - minReplicas: minReplicas - maxReplicas: maxReplicas - containerAppEnvId: containerAppEnv.outputs.containerAppEnvId - } -}] - resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { name: containerAppName location: resourceGroup().location diff --git a/src/spring/azext_spring/migration/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 similarity index 83% rename from src/spring/azext_spring/migration/templates/environment.bicep.j2 rename to src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index b7fae802e19..e2c55d2db0a 100644 --- a/src/spring/azext_spring/migration/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -6,17 +6,6 @@ param workloadProfileType string param daprAIInstrumentationKey string? = {{daprAIInstrumentationKey}} param daprAIConnectionString string? = {{daprAIConnectionString}} -module containerAppEnv 'environment.bicep' = { - name: 'containerAppEnvDeployment' - params: { - containerAppEnvName: containerAppEnvName - containerAppLogAnalyticsName: containerAppLogAnalyticsName - location: location - workloadProfileName: workloadProfileName - workloadProfileType: workloadProfileType - } -} - resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: containerAppLogAnalyticsName location: location 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..1cbaca607b1 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -0,0 +1,24 @@ +// Env +param containerAppEnvName string +param location string +param apps array +param workloadProfileName string +param workloadProfileType string + +module containerAppEnv 'environment.bicep' = { + name: 'containerAppEnvDeployment' + params: { + containerAppEnvName: containerAppEnvName + location: location + workloadProfileName: workloadProfileName + workloadProfileType: workloadProfileType + } +} + +module appModule 'app.bicep' = [for app in apps: { + name: '${app.name}' + dependsOn: [containerAppEnv] + params: { + containerAppEnvId: containerAppEnv.outputs.containerAppEnvId + } +}] \ No newline at end of file diff --git a/src/spring/azext_spring/migration/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 similarity index 100% rename from src/spring/azext_spring/migration/templates/param.bicepparam.j2 rename to src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 diff --git a/src/spring/azext_spring/migration/templates/readme_template.j2 b/src/spring/azext_spring/migration/converter/templates/readme_template.j2 similarity index 100% rename from src/spring/azext_spring/migration/templates/readme_template.j2 rename to src/spring/azext_spring/migration/converter/templates/readme_template.j2 diff --git a/src/spring/azext_spring/migration/templates/revision.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 similarity index 88% rename from src/spring/azext_spring/migration/templates/revision.bicep.j2 rename to src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 index 18a8adb943f..aa7e2c9d349 100644 --- a/src/spring/azext_spring/migration/templates/revision.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 @@ -24,5 +24,3 @@ resource conainerAppRevision 'Microsoft.App/containerApp/revisions@2024-03-01' = } } }] - -output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 4a7ca77e107..88b48ad8c39 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -41,7 +41,7 @@ def migration_aca_start(cmd, client, resource_group, service): converted_contents = context.run_converters(asa_arm) # Save each line of converted content to a separate file - context.save_to_files(converted_contents, 'output') + context.save_to_files(converted_contents, os.path.join("output","")) print("Succeed to generate Bicep files") def export_asa_arm_template(cmd, resource_group, service): @@ -69,18 +69,3 @@ def export_asa_arm_template(cmd, resource_group, service): def get_aca_bicep_params(aca_arm): return {"key1": "value1"} - -def replace_variables_in_template(template_path, output_path, variables): - if not os.path.exists(template_path): - raise FileNotFoundError(f"Template file {template_path} not found") - - with open(template_path, 'r', encoding='utf-8') as template_file: - content = template_file.read() - - for key, value in variables.items(): - placeholder = f"{{{{{key}}}}}" - content = content.replace(placeholder, f"'{value}'") - - os.makedirs(os.path.dirname(output_path), exist_ok=True) - with open(output_path, 'w', encoding='utf-8') as output_file: - output_file.write(content) From 4b3f760507adf4c2a025cfbde088db31c4209c4d Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 20 Jan 2025 18:08:25 +0800 Subject: [PATCH 08/98] Support multiple app modules & refactor conversion --- .../migration/converter/conversion_context.py | 27 ++++++++++--------- .../migration/converter/main_converter.py | 13 +++++++-- .../converter/templates/main.bicep.j2 | 11 ++++---- .../migration/migration_operations.py | 4 ++- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 0013105ab82..fab60051c12 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -29,34 +29,35 @@ def set_params_for_converter(self, converter_type, params): converter.set_params(params) def run_converters(self, source): - converted_contents = [] + converted_contents = {} source_wrapper = SourceDataWrapper(source) - # converted_contents.append(self.get_converter(MainConverter).convert(None)) - converted_contents.append( - self.get_converter(EnvironmentConverter).convert( - source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] - ) + converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert( + source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + ) + converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert( + source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] ) # converted_contents.append( # self.get_converter(AppConverter).convert( - # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps') + # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') # ) # ) # converted_contents.append( # self.get_converter(RevisionConverter).convert( - # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/apps/deployments') + # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') # ) # ) - converted_contents.append(self.get_converter(ReadMeConverter).convert(None)) + converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) return converted_contents def save_to_files(self, converted_contents, output_path): print("Start to save the converted content to files ...") os.makedirs(os.path.dirname(output_path), exist_ok=True) - for i, content in enumerate(converted_contents): - filename = f"{output_path}/export_script_{i+1}.bicep" - with open(filename, 'w', encoding='utf-8') as output_file: - print("Start to generate the {filename} file ...") + + for filename, content in converted_contents.items(): + output_filename = f"{output_path}/{filename}" + with open(output_filename, 'w', encoding='utf-8') as output_file: + print(f"Start to generate the {output_filename} file ...") output_file.write(content) class SourceDataWrapper: diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index d96edbfcee8..c257a2c1da1 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -3,10 +3,19 @@ # Concrete Converter Subclass for Read Me class MainConverter(ConverterTemplate): def load_source(self, source): - pass + self.source = [] + for resource in source: + print("resource: ", resource) + self.source.append(resource) def calculate_data(self): - pass + self.data.setdefault("apps", []) + for app in self.source: + self.data["apps"].append({ + "name": app["name"], + }) + print("app: ", app) + print(self.data["apps"]) def get_template_name(self): return "main.bicep" \ 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 index 1cbaca607b1..d0e9dd168a0 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -1,7 +1,7 @@ // Env param containerAppEnvName string param location string -param apps array + param workloadProfileName string param workloadProfileType string @@ -15,10 +15,11 @@ module containerAppEnv 'environment.bicep' = { } } -module appModule 'app.bicep' = [for app in apps: { - name: '${app.name}' - dependsOn: [containerAppEnv] +{% for app in data.apps %} +module {{ app.name }} 'app-{{ app.name }}.bicep' = { + name: '{{ app.name }}' params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId } -}] \ No newline at end of file +} +{% endfor %} \ 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 index 88b48ad8c39..d66a1a71f11 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -11,17 +11,19 @@ from .converter.revision_converter import RevisionConverter from .converter.gateway_converter import GatewayConverter from .converter.readme_converter import ReadMeConverter +from .converter.main_converter import MainConverter logger = get_logger(__name__) def migration_aca_start(cmd, client, resource_group, service): # API calls - print("Start API calls to get ASA service, apps and deployments...") + print("Start export ARM template for ASA service...") asa_arm = export_asa_arm_template(cmd, resource_group, service) # Create context and add converters context = ConversionContext() + context.add_converter(MainConverter()) context.add_converter(EnvironmentConverter()) context.add_converter(AppConverter()) context.add_converter(RevisionConverter()) From 1087cbfce166eef7fc85781a7b812be684172af4 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Mon, 20 Jan 2025 22:51:00 +0800 Subject: [PATCH 09/98] add ParamConverter and AppConverter && rename readme template (cherry picked from commit 7a0c5c6a422f6c6d709fef6cb8664db9fd03f36c) --- .../migration/converter/app_converter.py | 32 ++++++++++--------- .../migration/converter/conversion_context.py | 14 ++++---- .../converter/environment_converter.py | 5 +++ .../migration/converter/main_converter.py | 15 ++++++--- .../migration/converter/param_converter.py | 12 +++++++ .../migration/converter/readme_converter.py | 2 +- .../converter/templates/app.bicep.j2 | 18 +++++------ .../converter/templates/environment.bicep.j2 | 24 ++++++++------ .../converter/templates/main.bicep.j2 | 17 +++++----- .../converter/templates/param.bicepparam.j2 | 6 ++-- .../{readme_template.j2 => readme.md.j2} | 4 +-- .../migration/migration_operations.py | 2 ++ 12 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/param_converter.py rename src/spring/azext_spring/migration/converter/templates/{readme_template.j2 => readme.md.j2} (96%) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 4b9032b9303..32f822194fe 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -3,22 +3,24 @@ # Concrete Converter Subclass for Container App class AppConverter(ConverterTemplate): def load_source(self, source): - self.source = [] - for resource in source: - self.source.append(resource) + self.source = source def calculate_data(self): - self.data.apps = [] - for app in self.source: - self.data.apps.append({ - "containerAppName": app["name"], - "containerImage": self.params["container_image"], - "targetPort": self.params["target_port"], - "cpuCore": app['properties']["cpu_core"], - "memorySize": app["memory_size"], - "minReplicas": app["min_replicas"], - "maxReplicas": app["max_replicas"], - }) + appName = self.source['name'].split('/')[-1] + moduleName = appName.replace("-", "") + self.data = { + "containerAppName": appName, + "moduleName": moduleName, + "containerImage": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "targetPort": "80", + "cpuCore": "0.5", + "memorySize": "1", + "minReplicas": 1, + "maxReplicas": 5 + } def get_template_name(self): - return "app.bicep" \ No newline at end of file + return "app.bicep" + + def get_app_name(input_string): + return input_string.split('/')[-1] \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index fab60051c12..6572a19d1a1 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -8,6 +8,7 @@ from .revision_converter import RevisionConverter from .readme_converter import ReadMeConverter from .main_converter import MainConverter +from .param_converter import ParamConverter # Context Class class ConversionContext: @@ -37,16 +38,17 @@ def run_converters(self, source): converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert( source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] ) - # converted_contents.append( - # self.get_converter(AppConverter).convert( - # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - # ) - # ) + + for app in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps'): + appName = app['name'].split('/')[-1] + converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) + # converted_contents.append( # self.get_converter(RevisionConverter).convert( # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') # ) # ) + converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(None) converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) return converted_contents @@ -55,7 +57,7 @@ def save_to_files(self, converted_contents, output_path): os.makedirs(os.path.dirname(output_path), exist_ok=True) for filename, content in converted_contents.items(): - output_filename = f"{output_path}/{filename}" + output_filename = os.path.join(output_path, filename) with open(output_filename, 'w', encoding='utf-8') as output_file: print(f"Start to generate the {output_filename} file ...") 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 index f72ec44d219..654db92f639 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -10,6 +10,11 @@ def calculate_data(self): "containerAppEnvName": self.source['name'], "location": self.source['location'], "containerAppLogAnalyticsName": f"log-{self.source['name']}", + "daprAIInstrumentationKey": "", + "daprAIConnectionString": "", + "zoneRedundant": str(self.source['properties']['zoneRedundant']).lower(), + "infrastructureResourceGroup": self.source['properties'].get('infraResourceGroup'), + "mtlsEnabled": "true" } def get_template_name(self): diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index c257a2c1da1..a77874cfd43 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -5,17 +5,22 @@ class MainConverter(ConverterTemplate): def load_source(self, source): self.source = [] for resource in source: - print("resource: ", resource) self.source.append(resource) def calculate_data(self): self.data.setdefault("apps", []) - for app in self.source: + for item in self.source: + appName = item['name'].split('/')[-1] + moduleName = appName.replace("-", "") + templateName = f"{appName}_app.bicep" + + print(f"appName: {appName}, moduleName: {moduleName}, templateName: {templateName}") + self.data["apps"].append({ - "name": app["name"], + "appName": appName, + "moduleName": moduleName, + "templateName": templateName, }) - print("app: ", app) - print(self.data["apps"]) def get_template_name(self): return "main.bicep" \ No newline at end of file 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..a6c7a58993e --- /dev/null +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -0,0 +1,12 @@ +from .base_converter import ConverterTemplate + +# Concrete Converter Subclass for paramter +class ParamConverter(ConverterTemplate): + def load_source(self, source): + pass + + def calculate_data(self): + pass + + def get_template_name(self): + return "param.bicepparam" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 484cabc2a09..57c7ca28459 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -9,4 +9,4 @@ def calculate_data(self): pass def get_template_name(self): - return "readme_template" \ No newline at end of file + return "readme.md" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index 8abcdbbff85..d8497da0d34 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -1,21 +1,21 @@ -param containerAppName string = {{containerAppName}} -param containerImage string = {{containerImage}} -param targetPort int = {{targetPort}} +param containerAppName string = '{{data.containerAppName}}' +param containerImage string = '{{data.containerImage}}' +param targetPort int = {{data.targetPort}} -param cpuCore string = {{cpuCore}} -param memorySize string = {{memorySize}} +param cpuCore string = '{{data.cpuCore}}' +param memorySize string = '{{data.memorySize}}' @description('Minimum number of replicas that will be deployed') @minValue(0) -param minReplicas int = {{minReplicas}} +param minReplicas int = {{data.minReplicas}} @description('Maximum number of replicas that will be deployed') @minValue(1) -param maxReplicas int = {{maxReplicas}} +param maxReplicas int = {{data.maxReplicas}} param containerAppEnvId string -resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { +resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { name: containerAppName location: resourceGroup().location properties: { @@ -52,4 +52,4 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { } } -output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn +output containerAppFQDN string = {{data.moduleName}}.properties.configuration.ingress.fqdn diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index e2c55d2db0a..3471b895b39 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -1,10 +1,14 @@ -param containerAppEnvName string = {{containerAppEnvName}} -param containerAppLogAnalyticsName string = {{containerAppLogAnalyticsName}} -param location string = {{location}} +// Params param workloadProfileName string param workloadProfileType string -param daprAIInstrumentationKey string? = {{daprAIInstrumentationKey}} -param daprAIConnectionString string? = {{daprAIConnectionString}} +param minNodes int +param maxNodes int + +param containerAppEnvName string = '{{data.containerAppEnvName}}' +param containerAppLogAnalyticsName string = '{{data.containerAppLogAnalyticsName}}' +param location string = '{{data.location}}' +param daprAIInstrumentationKey string = '{{data.daprAIInstrumentationKey}}' +param daprAIConnectionString string = '{{data.daprAIConnectionString}}' resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: containerAppLogAnalyticsName @@ -30,22 +34,22 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { sharedKey: logAnalytics.listKeys().primarySharedKey } } - zoneRedundant: '{{zoneRedundant}}' + zoneRedundant: {{data.zoneRedundant}} // customDomainConfiguration: {} workloadProfiles: [ { name: workloadProfileName workloadProfileType: workloadProfileType - minimumCount: '{{minimumCount}}' - maximumCount: '{{maximumCount}}' + minimumCount: minNodes + maximumCount: maxNodes } ] // kedaConfiguration: {} // daprConfiguration: {} - infrastructureResourceGroup: '{{infrastructureResourceGroup}}' + infrastructureResourceGroup: '{{data.infrastructureResourceGroup}}' peerAuthentication: { mtls: { - enabled: '{{mtlsEnabled}}' + enabled: {{data.mtlsEnabled}} } } //peerTrafficConfiguration: {} diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index d0e9dd168a0..33e3a3c2930 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -1,23 +1,22 @@ -// Env -param containerAppEnvName string -param location string - +// Params param workloadProfileName string param workloadProfileType string +param minNodes int +param maxNodes int module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' params: { - containerAppEnvName: containerAppEnvName - location: location workloadProfileName: workloadProfileName workloadProfileType: workloadProfileType + minNodes: minNodes + maxNodes: maxNodes } } -{% for app in data.apps %} -module {{ app.name }} 'app-{{ app.name }}.bicep' = { - name: '{{ app.name }}' +{% for item in data.apps %} +module {{ item.moduleName }} '{{ item.templateName }}' = { + name: '{{ item.appName }}' params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId } diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 62c211dceb9..89dcd22b918 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -1,4 +1,6 @@ using './main.bicep' -param workloadProfileName = 'Consumption' -param workloadProfileType = 'Consumption' \ No newline at end of file +param workloadProfileName = 'Dedicate' +param workloadProfileType = 'Dedicate' +param minNodes = 1 +param maxNodes = 3 \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/readme_template.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 similarity index 96% rename from src/spring/azext_spring/migration/converter/templates/readme_template.j2 rename to src/spring/azext_spring/migration/converter/templates/readme.md.j2 index 173da9b46cb..46dcd875d31 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme_template.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -30,8 +30,8 @@ Deploy the Bicep files using the Azure CLI: az deployment group create \ --resource-group \ - --template-file \ - --parameters .json + --template-file main.bicep \ + --parameters param.bicepparam Replace .json with the path to your parameter file. diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index d66a1a71f11..5f8773d23af 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -12,6 +12,7 @@ from .converter.gateway_converter import GatewayConverter from .converter.readme_converter import ReadMeConverter from .converter.main_converter import MainConverter +from .converter.param_converter import ParamConverter logger = get_logger(__name__) @@ -29,6 +30,7 @@ def migration_aca_start(cmd, client, resource_group, service): context.add_converter(RevisionConverter()) context.add_converter(GatewayConverter(client)) context.add_converter(ReadMeConverter()) + context.add_converter(ParamConverter()) # Define the parameters for the Bicep template and output the Bicep files print("Start to generate ACA bicep files based on parameters...") From 85d61664e05901c495e0d976d38c9ca8fc66a028 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 21 Jan 2025 02:47:03 +0800 Subject: [PATCH 10/98] create container apps env successfully --- .../migration/converter/templates/param.bicepparam.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 89dcd22b918..a6e3d211a64 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -1,6 +1,6 @@ using './main.bicep' -param workloadProfileName = 'Dedicate' -param workloadProfileType = 'Dedicate' +param workloadProfileName = 'myprofile' +param workloadProfileType = 'D4' param minNodes = 1 -param maxNodes = 3 \ No newline at end of file +param maxNodes = 2 \ No newline at end of file From 5e348c1feef2085bb8ea234db22e50b4ffdb1136 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 21 Jan 2025 03:31:05 +0800 Subject: [PATCH 11/98] align the locaton value in bicep file --- .../migration/converter/environment_converter.py | 1 - .../migration/converter/templates/environment.bicep.j2 | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 654db92f639..fc2cf08c9c2 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -8,7 +8,6 @@ def load_source(self, source): def calculate_data(self): self.data = { "containerAppEnvName": self.source['name'], - "location": self.source['location'], "containerAppLogAnalyticsName": f"log-{self.source['name']}", "daprAIInstrumentationKey": "", "daprAIConnectionString": "", diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 3471b895b39..465d22faa64 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -6,13 +6,12 @@ param maxNodes int param containerAppEnvName string = '{{data.containerAppEnvName}}' param containerAppLogAnalyticsName string = '{{data.containerAppLogAnalyticsName}}' -param location string = '{{data.location}}' param daprAIInstrumentationKey string = '{{data.daprAIInstrumentationKey}}' param daprAIConnectionString string = '{{data.daprAIConnectionString}}' resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: containerAppLogAnalyticsName - location: location + location: resourceGroup().location properties: { sku: { name: 'PerGB2018' @@ -22,7 +21,7 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { name: containerAppEnvName - location: location + location: resourceGroup().location properties: { daprAIInstrumentationKey: daprAIInstrumentationKey daprAIConnectionString: daprAIConnectionString From 8a6a6a0b46d492edae27d0595d9e7408b21dea6f Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Fri, 24 Jan 2025 17:37:49 +0800 Subject: [PATCH 12/98] Support gateway bicep --- .../migration/converter/conversion_context.py | 16 ++++++ .../migration/converter/gateway_converter.py | 54 +++++++++++++++++-- .../converter/templates/environment.bicep.j2 | 1 + .../converter/templates/gateway.bicep.j2 | 42 +++++++++++++++ .../converter/templates/main.bicep.j2 | 11 +++- .../migration/migration_operations.py | 7 ++- 6 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 6572a19d1a1..836ab943398 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -9,6 +9,7 @@ from .readme_converter import ReadMeConverter from .main_converter import MainConverter from .param_converter import ParamConverter +from .gateway_converter import GatewayConverter # Context Class class ConversionContext: @@ -50,6 +51,21 @@ def run_converters(self, source): # ) converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(None) converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) + + for gateway in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways'): + gateway_name = gateway['name'].split('/')[-1] + gateway_key = gateway_name+"_"+self.get_converter(GatewayConverter).get_template_name() + routes = [] + for gateway_route in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways/routeConfigs'): + routes.append(gateway_route) + gateway_source = { + "gateway": gateway, + "routes": routes, + } + converted_contents[gateway_key] = self.get_converter(GatewayConverter).convert(gateway_source) + print("converted_contents for gateway: \n", converted_contents[gateway_key]) + break + return converted_contents def save_to_files(self, converted_contents, output_path): diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 062646b94e2..724bea17c20 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -2,15 +2,61 @@ # Concrete Converter Subclass for Gateway class GatewayConverter(ConverterTemplate): - def __init__(self, client): + DEFAULT_NAME = "default" + + def __init__(self, client, resource_group, service): + super().__init__() self.client = client + self.resource_group = resource_group + self.service = service def load_source(self, source): - # Call the client to get additional data - pass + self.source = source + secret_envs_dict = self.client.gateways.list_env_secrets(self.resource_group, self.service, self.DEFAULT_NAME) + self.source['secretEnvs'] = secret_envs_dict + print(f"source: {self.source}") def calculate_data(self): - pass + gatewayName = self.source['gateway']['name'].split('/')[-1] + self.data = { + "gatewayName": gatewayName, + "configurations": [ + { + "propertyName": "spring.cloud.gateway.xxx", + "value": "abcd", + }, + { + "propertyName": "spring.cloud.gateway.yyy", + "value": "efgh", + } + ], + "replicas": 2, + "routes": [ + { + "id": "route1", + "uri": "http://localhost:8080", + "predicates": [ + "/api/v1/**", + "/api/v2/**" + ], + "filters": [ + "AddRequestHeader=Host, example.com", + "AddRequestParameter=example, example.com" + ], + "order": 1 + }, + { + "id": "route2", + "uri": "http://localhost:8081", + "predicates": [ + "/api/v3/**", + "/api/v4/**" + ], + "filters": [ "AddRequestHeader=Host, example.com", "AddRequestParameter=example, example.com"], + "order": 2 + } + ] + } def get_template_name(self): return "gateway.bicep" \ 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 index 465d22faa64..eedcb83d3ab 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -56,3 +56,4 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { } output containerAppEnvId string = containerAppEnv.id +output containerAppEnvName string = containerAppEnv.name 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..581d2c69a9f --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 @@ -0,0 +1,42 @@ +param managedEnvironments_ninpan_aca_env_name string + +resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { + name: '${managedEnvironments_ninpan_aca_env_name}/{{ data.gatewayName }}' + dependsOn: [ + resourceId('Microsoft.App/managedEnvironments', managedEnvironments_ninpan_aca_env_name) + ] + 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 index 33e3a3c2930..38a96c7db01 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -4,7 +4,7 @@ param workloadProfileType string param minNodes int param maxNodes int -module containerAppEnv 'environment.bicep' = { +module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' params: { workloadProfileName: workloadProfileName @@ -21,4 +21,11 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId } } -{% endfor %} \ No newline at end of file +{% endfor %} + +module managedGateway 'gateway.bicep' = { + name: 'gatewayDeployment' + params: { + managedEnvironments_ninpan_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} \ 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 index 5f8773d23af..eadf3660dfd 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -28,7 +28,7 @@ def migration_aca_start(cmd, client, resource_group, service): context.add_converter(EnvironmentConverter()) context.add_converter(AppConverter()) context.add_converter(RevisionConverter()) - context.add_converter(GatewayConverter(client)) + context.add_converter(GatewayConverter(client, resource_group, service)) context.add_converter(ReadMeConverter()) context.add_converter(ParamConverter()) @@ -42,12 +42,14 @@ def migration_aca_start(cmd, client, resource_group, service): context.set_params_for_converter(EnvironmentConverter, main_bicep_params) # Run all converters + print("Start to run all converters...") converted_contents = context.run_converters(asa_arm) # Save each line of converted content to a separate file context.save_to_files(converted_contents, os.path.join("output","")) print("Succeed to generate Bicep files") + def export_asa_arm_template(cmd, resource_group, service): resources = [] subscription = get_subscription_id(cmd.cli_ctx) @@ -71,5 +73,6 @@ def export_asa_arm_template(cmd, resource_group, service): parameters=export_template_request) return result.template -def get_aca_bicep_params(aca_arm): + +def get_aca_bicep_params(asa_arm): return {"key1": "value1"} From 5295f8546d6ba7a52c28d9db7fc8d117e630a40a Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Fri, 7 Feb 2025 18:14:48 +0800 Subject: [PATCH 13/98] Support managed components bicep --- .../migration/converter/acs_converter.py | 76 ++++++++++++++++ .../converter/config_server_converter.py | 74 +++++++++++++++ .../migration/converter/conversion_context.py | 86 ++++++++++++++---- .../migration/converter/eureka_converter.py | 25 ++++++ .../migration/converter/gateway_converter.py | 89 +++++++++++-------- .../converter/live_view_converter.py | 25 ++++++ .../converter/service_registry_converter.py | 25 ++++++ .../templates/config_server.bicep.j2 | 23 +++++ .../converter/templates/eureka.bicep.j2 | 23 +++++ .../converter/templates/gateway.bicep.j2 | 6 +- .../converter/templates/main.bicep.j2 | 23 ++++- .../converter/templates/readme.md.j2 | 2 + .../templates/spring_boot_admin.bicep.j2 | 23 +++++ .../migration/migration_operations.py | 18 +++- 14 files changed, 455 insertions(+), 63 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/acs_converter.py create mode 100644 src/spring/azext_spring/migration/converter/config_server_converter.py create mode 100644 src/spring/azext_spring/migration/converter/eureka_converter.py create mode 100644 src/spring/azext_spring/migration/converter/live_view_converter.py create mode 100644 src/spring/azext_spring/migration/converter/service_registry_converter.py create mode 100644 src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 create mode 100644 src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 create mode 100644 src/spring/azext_spring/migration/converter/templates/spring_boot_admin.bicep.j2 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..c17e9e31a63 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -0,0 +1,76 @@ +from .base_converter import ConverterTemplate + +# Concrete Converter Subclass for Config Server +class ACSConverter(ConverterTemplate): + + CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" + KEY_URI = ".uri" + KEY_LABEL = ".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): + super().__init__() + + def load_source(self, source): + self.source = source + print(f"source: {self.source}") + + def calculate_data(self): + name = self.source['name'].split('/')[-1] + print(f"ACS source: {self.source}") + configurations = self._get_configurations(self.source) + replicas = 2 + + self.data = { + "configServerName": name, + "configurations": configurations, + "replicas": replicas + } + + def get_template_name(self): + return "config_server.bicep" + + def _get_configurations(self, source): + configurations = [] + git_repos = source['properties']['settings']['gitProperty']['repositories'] + + if len(git_repos) > 0: + default_repo = git_repos[0] + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, default_repo.get('uri')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, default_repo.get('label')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, default_repo.get('searchPaths')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, default_repo.get('username')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, default_repo.get('password')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, default_repo.get('privateKey')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, default_repo.get('hostKey')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, default_repo.get('hostKeyAlgorithm')) + + 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_existss(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm')) + + return configurations + + def _add_property_if_existss(self, configurations, key, value): + if value: + if isinstance(value, (list, tuple)): + value = ",".join(map(str, value)) + configurations.append({ + "propertyName": key, + "value": value + }) + \ No newline at end of file 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..cdbc9e94cdd --- /dev/null +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -0,0 +1,74 @@ +from .base_converter import ConverterTemplate + +# Concrete Converter Subclass for Config Server +class ConfigServerConverter(ConverterTemplate): + + CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" + KEY_URI = ".uri" + KEY_LABEL = ".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): + super().__init__() + + def load_source(self, source): + self.source = source + + def calculate_data(self): + name = self.source['name'].split('/')[-1] + configurations = self._get_configurations(self.source) + replicas = 2 + + self.data = { + "configServerName": name, + "configurations": configurations, + "replicas": replicas + } + + def get_template_name(self): + return "config_server.bicep" + + def _get_configurations(self, source): + configurations = [] + + git_property = source['properties']['configServer']['gitProperty'] + git_repos = git_property['repositories'] + + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, git_property.get('uri')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, git_property.get('label')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, git_property.get('searchPaths')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, git_property.get('username')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, git_property.get('password')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, git_property.get('privateKey')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, git_property.get('hostKey')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, git_property.get('hostKeyAlgorithm')) + + for repo in git_repos: + configuration_key_repo_prefix = self.CONFIGURATION_KEY_PREFIX + ".repos." + repo['name'] + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PATTERN, repo.get('pattern')) + + return configurations + + def _add_property_if_existss(self, configurations, key, value): + if value: + if isinstance(value, (list, tuple)): + value = ",".join(map(str, value)) + configurations.append({ + "propertyName": key, + "value": value + }) + \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 836ab943398..77c00d15dd7 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -1,7 +1,7 @@ import os from abc import ABC, abstractmethod -from jinja2 import Template +from knack.log import get_logger from .base_converter import ConverterTemplate from .environment_converter import EnvironmentConverter from .app_converter import AppConverter @@ -10,6 +10,13 @@ from .main_converter import MainConverter from .param_converter import ParamConverter from .gateway_converter import GatewayConverter +from .eureka_converter import EurekaConverter +from .service_registry_converter import ServiceRegistryConverter +from .config_server_converter import ConfigServerConverter +from .acs_converter import ACSConverter +from .live_view_converter import LiveViewConverter + +logger = get_logger(__name__) # Context Class class ConversionContext: @@ -40,10 +47,12 @@ def run_converters(self, source): source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] ) + asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + for app in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps'): - appName = app['name'].split('/')[-1] + appName = app['name'].split('/')[-1][:-3] converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) - + # converted_contents.append( # self.get_converter(RevisionConverter).convert( # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') @@ -52,9 +61,26 @@ def run_converters(self, source): converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(None) converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) + converted_contents = self._convert_gateway(source_wrapper, converted_contents) + converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, asa_service) + converted_contents = self._convert_live_view(source_wrapper, converted_contents) + converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service) + + return converted_contents + + def save_to_files(self, converted_contents, output_path): + print("Start to save the converted content to files ...") + os.makedirs(os.path.dirname(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: + print(f"Start to generate the {output_filename} file ...") + output_file.write(content) + + def _convert_gateway(self, source_wrapper, converted_contents): for gateway in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways'): - gateway_name = gateway['name'].split('/')[-1] - gateway_key = gateway_name+"_"+self.get_converter(GatewayConverter).get_template_name() + gateway_key = self.get_converter(GatewayConverter).get_template_name() routes = [] for gateway_route in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways/routeConfigs'): routes.append(gateway_route) @@ -63,24 +89,52 @@ def run_converters(self, source): "routes": routes, } converted_contents[gateway_key] = self.get_converter(GatewayConverter).convert(gateway_source) - print("converted_contents for gateway: \n", converted_contents[gateway_key]) - break + logger.info(f"converted_contents for gateway: {converted_contents[gateway_key]}") + return converted_contents + + def _convert_config_server_and_ACS(self, source_wrapper, converted_contents, asa_service): + enabled_config_server = False + + for config_server in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers'): + enabled_config_server = True + config_key = self.get_converter(ConfigServerConverter).get_template_name() + converted_contents[config_key] = self.get_converter(ConfigServerConverter).convert(config_server) + logger.debug(f"converted_contents for config server: {converted_contents[config_key]}") + + if not enabled_config_server: + for acs in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices'): + config_key = self.get_converter(ACSConverter).get_template_name() + converted_contents[config_key] = self.get_converter(ACSConverter).convert(acs) + logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[config_key]}") return converted_contents - def save_to_files(self, converted_contents, output_path): - print("Start to save the converted content to files ...") - os.makedirs(os.path.dirname(output_path), exist_ok=True) + def _convert_live_view(self, source_wrapper, converted_contents): + for live_view in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews'): + live_view_key = self.get_converter(LiveViewConverter).get_template_name() + converted_contents[live_view_key] = self.get_converter(LiveViewConverter).convert(live_view) + logger.info(f"converted_contents for Live View: {converted_contents[live_view_key]}") + return converted_contents - 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: - print(f"Start to generate the {output_filename} file ...") - output_file.write(content) + def _convert_eureka_and_service_registry(self, source_wrapper, converted_contents, asa_service): + is_enterprise_tier = self._is_enterprise_tier(asa_service) + for service_registry in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries'): + eureka_key = self.get_converter(ServiceRegistryConverter).get_template_name() + converted_contents[eureka_key] = self.get_converter(ServiceRegistryConverter).convert(service_registry) + logger.info(f"converted_contents for Service Registry: {converted_contents[eureka_key]}") + return converted_contents + + if not is_enterprise_tier: + eureka_key = self.get_converter(EurekaConverter).get_template_name() + converted_contents[eureka_key] = self.get_converter(EurekaConverter).convert() + return converted_contents + + def _is_enterprise_tier(self, asa_service): + return asa_service['sku']['tier'] == 'Enterprise' 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] \ No newline at end of file + return [resource for resource in self.source['resources'] if resource['type'] == resource_type] 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..88993844f3f --- /dev/null +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -0,0 +1,25 @@ +from .base_converter import ConverterTemplate + +class EurekaConverter(ConverterTemplate): + + def __init__(self): + super().__init__() + + def load_source(self): + pass + + def calculate_data(self): + name = self.source['name'].split('/')[-1] + configurations = [] + replicas = 1 + + self.data = { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + + 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 index 724bea17c20..6a888fc82f6 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -1,6 +1,5 @@ from .base_converter import ConverterTemplate -# Concrete Converter Subclass for Gateway class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" @@ -14,49 +13,61 @@ def load_source(self, source): self.source = source secret_envs_dict = self.client.gateways.list_env_secrets(self.resource_group, self.service, self.DEFAULT_NAME) self.source['secretEnvs'] = secret_envs_dict - print(f"source: {self.source}") def calculate_data(self): gatewayName = self.source['gateway']['name'].split('/')[-1] + configurations = self._get_configurations(self.source) + replicas = min(2, self.source['gateway']['sku']['capacity']) + routes = self._get_routes(self.source['routes']) + self.data = { "gatewayName": gatewayName, - "configurations": [ - { - "propertyName": "spring.cloud.gateway.xxx", - "value": "abcd", - }, - { - "propertyName": "spring.cloud.gateway.yyy", - "value": "efgh", - } - ], - "replicas": 2, - "routes": [ - { - "id": "route1", - "uri": "http://localhost:8080", - "predicates": [ - "/api/v1/**", - "/api/v2/**" - ], - "filters": [ - "AddRequestHeader=Host, example.com", - "AddRequestParameter=example, example.com" - ], - "order": 1 - }, - { - "id": "route2", - "uri": "http://localhost:8081", - "predicates": [ - "/api/v3/**", - "/api/v4/**" - ], - "filters": [ "AddRequestHeader=Host, example.com", "AddRequestParameter=example, example.com"], - "order": 2 - } - ] + "configurations": configurations, + "replicas": replicas, + "routes": routes, } + def _get_configurations(self, source): + configurations = [] + for key, value in source['gateway']['properties']['environmentVariables']['properties'].items(): + configurations.append({ + "propertyName": key, + "value": value, + }) + for key, value in source['secretEnvs'].items(): + configurations.append({ + "propertyName": key, + "value": value, + }) + return configurations + + def _get_routes(self, routes): + aca_routes = [] + for route in routes: + aca_id = route['name'].split('/')[-1] + aca_uri = self._get_uri_from_route(route) + for r in route['properties']['routes']: + aca_routes.append({ + "id": aca_id, + "uri": r.get('uri', aca_uri), + "predicates": r.get('predicates'), + "filters": r.get('filters'), + "order": r.get('order') + }) + return aca_routes + + def _get_uri_from_route(self, route): + app_resource_id = route['properties']['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_template_name(self): - return "gateway.bicep" \ No newline at end of file + 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..2b8916587c7 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -0,0 +1,25 @@ +from .base_converter import ConverterTemplate + +class LiveViewConverter(ConverterTemplate): + + def __init__(self): + super().__init__() + + def load_source(self, source): + self.source = source + + def calculate_data(self): + name = self.source['name'].split('/')[-1] + configurations = [] + replicas = 1 + + self.data = { + "sbaName": name, + "configurations": configurations, + "replicas": replicas + } + + def get_template_name(self): + return "spring_boot_admin.bicep" + + 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..442a4c80c30 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -0,0 +1,25 @@ +from .base_converter import ConverterTemplate + +class ServiceRegistryConverter(ConverterTemplate): + + def __init__(self): + super().__init__() + + def load_source(self, source): + self.source = source + + def calculate_data(self): + name = self.source['name'].split('/')[-1] + configurations = [] + replicas = 1 + + self.data = { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + + def get_template_name(self): + return "eureka.bicep" + + 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..1bf7c79eff2 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -0,0 +1,23 @@ +param managedEnvironments_aca_env_name string + +resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.configServerName }}' + dependsOn: [ + resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) + ] + properties: { + componentType: 'SpringCloudConfig' + 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/eureka.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 new file mode 100644 index 00000000000..9125606bafe --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 @@ -0,0 +1,23 @@ +param managedEnvironments_aca_env_name string + +resource eurekaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.eurekaName }}' + dependsOn: [ + resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) + ] + 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 index 581d2c69a9f..a844e132c16 100644 --- a/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 @@ -1,9 +1,9 @@ -param managedEnvironments_ninpan_aca_env_name string +param managedEnvironments_aca_env_name string resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { - name: '${managedEnvironments_ninpan_aca_env_name}/{{ data.gatewayName }}' + name: '${managedEnvironments_aca_env_name}/{{ data.gatewayName }}' dependsOn: [ - resourceId('Microsoft.App/managedEnvironments', managedEnvironments_ninpan_aca_env_name) + resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) ] properties: { componentType: 'SpringCloudGateway' diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 38a96c7db01..69625a50a98 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -26,6 +26,27 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { module managedGateway 'gateway.bicep' = { name: 'gatewayDeployment' params: { - managedEnvironments_ninpan_aca_env_name: containerAppEnv.outputs.containerAppEnvName + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} + +module managedConfig 'config_server.bicep' = { + name: 'configServerDeployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} + +module managedEureka 'eureka.bicep' = { + name: 'eurekaDeployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} + +module managedSpringBootAdmin 'spring_boot_admin.bicep' = { + name: 'springBootAdminDeployment' + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } \ 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 index 46dcd875d31..7ff6fb0b987 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -24,6 +24,8 @@ If the resource group specified in the Bicep files does not already exist, creat az group create --name --location +Replace the "*" with the correct credentials in config server bicep file + 3. Deploy the Bicep Files Deploy the Bicep files using the Azure CLI: 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..8e59a8e19d6 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/templates/spring_boot_admin.bicep.j2 @@ -0,0 +1,23 @@ +param managedEnvironments_aca_env_name string + +resource sbaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { + name: '${managedEnvironments_aca_env_name}/{{ data.sbaName }}' + dependsOn: [ + resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) + ] + 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 index eadf3660dfd..bb23074f954 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -10,6 +10,11 @@ from .converter.app_converter import AppConverter from .converter.revision_converter import RevisionConverter 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 @@ -19,7 +24,7 @@ def migration_aca_start(cmd, client, resource_group, service): # API calls - print("Start export ARM template for ASA service...") + logger.info("Start export ARM template for ASA service...") asa_arm = export_asa_arm_template(cmd, resource_group, service) # Create context and add converters @@ -29,11 +34,16 @@ def migration_aca_start(cmd, client, resource_group, service): context.add_converter(AppConverter()) context.add_converter(RevisionConverter()) context.add_converter(GatewayConverter(client, resource_group, service)) + context.add_converter(EurekaConverter()) + context.add_converter(ServiceRegistryConverter()) + context.add_converter(ConfigServerConverter()) + context.add_converter(ACSConverter()) + context.add_converter(LiveViewConverter()) context.add_converter(ReadMeConverter()) context.add_converter(ParamConverter()) # Define the parameters for the Bicep template and output the Bicep files - print("Start to generate ACA bicep files based on parameters...") + logger.info("Start to generate ACA bicep files based on parameters...") # Prepare bicep parameters main_bicep_params = get_aca_bicep_params(asa_arm) @@ -42,12 +52,12 @@ def migration_aca_start(cmd, client, resource_group, service): context.set_params_for_converter(EnvironmentConverter, main_bicep_params) # Run all converters - print("Start to run all converters...") + logger.info("Start to run all converters...") converted_contents = context.run_converters(asa_arm) # Save each line of converted content to a separate file context.save_to_files(converted_contents, os.path.join("output","")) - print("Succeed to generate Bicep files") + logger.info("Succeed to generate Bicep files") def export_asa_arm_template(cmd, resource_group, service): From 14a203ae5b8a47d5157fc8d8fce791a4b468689b Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 10 Feb 2025 16:03:36 +0800 Subject: [PATCH 14/98] Add maintenance window --- .../converter/environment_converter.py | 23 +++++++---- .../converter/templates/environment.bicep.j2 | 41 ++++++++++--------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index fc2cf08c9c2..67524a5131b 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -6,15 +6,24 @@ def load_source(self, source): self.source = source def calculate_data(self): + name = self.source['name'].split('/')[-1] self.data = { - "containerAppEnvName": self.source['name'], - "containerAppLogAnalyticsName": f"log-{self.source['name']}", - "daprAIInstrumentationKey": "", - "daprAIConnectionString": "", - "zoneRedundant": str(self.source['properties']['zoneRedundant']).lower(), - "infrastructureResourceGroup": self.source['properties'].get('infraResourceGroup'), - "mtlsEnabled": "true" + "containerAppEnvName": name, + "containerAppLogAnalyticsName": f"log-{name}", } + asa_zone_redundant = self.source['properties'].get('zoneRedundant') + if asa_zone_redundant is not None: + self.data["zoneRedundant"] = str(asa_zone_redundant).lower() + + asa_maintenance_window = self.source['properties'].get('maintenanceScheduleConfiguration') + if asa_maintenance_window: + aca_maintenance_window = [{ + "weekDay": asa_maintenance_window['day'], + "startHourUtc": asa_maintenance_window['hour'], + "durationHours": 8, + }] + self.data["scheduledEntries"] = aca_maintenance_window + def get_template_name(self): return "environment.bicep" \ 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 index eedcb83d3ab..4b1d19ffc7d 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -1,13 +1,12 @@ // Params -param workloadProfileName string -param workloadProfileType string -param minNodes int -param maxNodes int +param workloadProfileName string = 'Consumption' +param workloadProfileType string = 'Consumption' +param minNodes int = 1 +param maxNodes int = 4 -param containerAppEnvName string = '{{data.containerAppEnvName}}' +param managedEnvName string = '{{data.containerAppEnvName}}' param containerAppLogAnalyticsName string = '{{data.containerAppLogAnalyticsName}}' -param daprAIInstrumentationKey string = '{{data.daprAIInstrumentationKey}}' -param daprAIConnectionString string = '{{data.daprAIConnectionString}}' +param zoneRedundant = '{{data.zoneRedundant}}' resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: containerAppLogAnalyticsName @@ -20,11 +19,9 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { } resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { - name: containerAppEnvName + name: managedEnvName location: resourceGroup().location properties: { - daprAIInstrumentationKey: daprAIInstrumentationKey - daprAIConnectionString: daprAIConnectionString // vnetConfiguration: {} appLogsConfiguration: { destination: 'log-analytics' @@ -33,7 +30,7 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { sharedKey: logAnalytics.listKeys().primarySharedKey } } - zoneRedundant: {{data.zoneRedundant}} + zoneRedundant: zoneRedundant // customDomainConfiguration: {} workloadProfiles: [ { @@ -43,17 +40,21 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { maximumCount: maxNodes } ] - // kedaConfiguration: {} - // daprConfiguration: {} - infrastructureResourceGroup: '{{data.infrastructureResourceGroup}}' - peerAuthentication: { - mtls: { - enabled: {{data.mtlsEnabled}} - } - } - //peerTrafficConfiguration: {} } } +{% if data.scheduledEntries %} +resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigurations@2024-10-02-Preview' = { + name: '${managedEnvName}/default' + dependsOn: [ + resourceId('Microsoft.App/managedEnvironments', managedEnvName) + ] + location: resourceGroup().location + properties: { + scheduledEntries: {{ data.scheduledEntries }} + } +} +{% endif %} + output containerAppEnvId string = containerAppEnv.id output containerAppEnvName string = containerAppEnv.name From 6f9f9b2e3672da671d4dd16b7a609db68336edaa Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Feb 2025 00:48:29 +0800 Subject: [PATCH 15/98] Add app bind --- src/spring/azext_spring/_params.py | 1 + .../migration/converter/acs_converter.py | 4 +- .../migration/converter/app_converter.py | 51 ++++++++++++++++- .../converter/config_server_converter.py | 4 +- .../migration/converter/conversion_context.py | 12 ++-- .../migration/converter/eureka_converter.py | 2 +- .../migration/converter/gateway_converter.py | 4 +- .../converter/live_view_converter.py | 2 +- .../migration/converter/main_converter.py | 2 - .../converter/service_registry_converter.py | 2 +- .../converter/templates/app.bicep.j2 | 8 +++ .../templates/config_server.bicep.j2 | 7 +-- .../converter/templates/environment.bicep.j2 | 22 ++++---- .../converter/templates/eureka.bicep.j2 | 7 +-- .../converter/templates/gateway.bicep.j2 | 13 ++--- .../converter/templates/main.bicep.j2 | 21 +++++-- .../converter/templates/param.bicepparam.j2 | 6 +- .../converter/templates/readme.md.j2 | 55 ++++++++++++------- .../templates/spring_boot_admin.bicep.j2 | 7 +-- .../migration/migration_operations.py | 17 +++--- src/spring/azext_spring/spring_instance.py | 4 +- 21 files changed, 157 insertions(+), 94 deletions(-) diff --git a/src/spring/azext_spring/_params.py b/src/spring/azext_spring/_params.py index 9090f50eed0..fca57db24f2 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -1310,3 +1310,4 @@ def prepare_common_logs_argument(c): with self.argument_context('spring migration-aca start') as c: c.argument('service', service_name_type) + c.argument('output_folder', help='The output folder for the generated Bicep files.') diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index c17e9e31a63..96aab2048ec 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -19,11 +19,9 @@ def __init__(self): def load_source(self, source): self.source = source - print(f"source: {self.source}") def calculate_data(self): - name = self.source['name'].split('/')[-1] - print(f"ACS source: {self.source}") + name = f"config" configurations = self._get_configurations(self.source) replicas = 2 diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 32f822194fe..919e5bd3569 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -1,13 +1,22 @@ +import re + from .base_converter import ConverterTemplate # Concrete Converter Subclass for Container App class AppConverter(ConverterTemplate): def load_source(self, source): self.source = source + self.source['enabled_sba'] = False + # print(f"App source: {self.source}") def calculate_data(self): + enable_sba = False appName = self.source['name'].split('/')[-1] + envName = self.source['name'].split('/')[0] moduleName = appName.replace("-", "") + containers = [] + serviceBinds, dependsOn = self._get_service_bind(self.source, envName) + self.data = { "containerAppName": appName, "moduleName": moduleName, @@ -16,6 +25,11 @@ def calculate_data(self): "cpuCore": "0.5", "memorySize": "1", "minReplicas": 1, + "dependsOn": dependsOn, + "template": { + "containers": containers, + "serviceBinds": serviceBinds, + }, "maxReplicas": 5 } @@ -23,4 +37,39 @@ def get_template_name(self): return "app.bicep" def get_app_name(input_string): - return input_string.split('/')[-1] \ No newline at end of file + return input_string.split('/')[-1] + + def _get_service_bind(self, source, envName): + enable_config = False + enable_eureka = False + enable_sba = source['enabled_sba'] + dependsOn = [] + service_bind = [] + addon = source['properties'].get('addonConfigs') + + if addon is None: + return None + + if addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None \ + or addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None: + service_bind.append({ + "name": "bind-config", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" + }) + enable_config = True + if addon.get('serviceRegistry') is not None and addon['serviceRegistry'].get('resourceId') is not None: + service_bind.append({ + "name": "bind-eureka", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" + }) + enable_eureka = True + if enable_sba: + service_bind.append({ + "name": "bind-sba", + "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'admin')" + }) + if enable_config: + dependsOn.append("managedConfig") + # print(f"Service bind: {service_bind}") + return service_bind, dependsOn + diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index cdbc9e94cdd..61a25991310 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -5,7 +5,7 @@ class ConfigServerConverter(ConverterTemplate): CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" KEY_URI = ".uri" - KEY_LABEL = ".label" + KEY_LABEL = ".default-label" KEY_SEARCH_PATHS = ".search-paths" KEY_USERNAME = ".username" KEY_PASSWORD = ".password" @@ -21,7 +21,7 @@ def load_source(self, source): self.source = source def calculate_data(self): - name = self.source['name'].split('/')[-1] + name = f"config" configurations = self._get_configurations(self.source) replicas = 2 diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 77c00d15dd7..ed44feb17ef 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -50,7 +50,7 @@ def run_converters(self, source): asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] for app in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps'): - appName = app['name'].split('/')[-1][:-3] + appName = app['name'].split('/')[-1] converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) # converted_contents.append( @@ -62,20 +62,20 @@ def run_converters(self, source): converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) converted_contents = self._convert_gateway(source_wrapper, converted_contents) - converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, asa_service) + converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents) converted_contents = self._convert_live_view(source_wrapper, converted_contents) converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service) return converted_contents def save_to_files(self, converted_contents, output_path): - print("Start to save the converted content to files ...") - os.makedirs(os.path.dirname(output_path), exist_ok=True) + 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: - print(f"Start to generate the {output_filename} file ...") + logger.info(f"Generating the file {output_filename}...") output_file.write(content) def _convert_gateway(self, source_wrapper, converted_contents): @@ -92,7 +92,7 @@ def _convert_gateway(self, source_wrapper, converted_contents): logger.info(f"converted_contents for gateway: {converted_contents[gateway_key]}") return converted_contents - def _convert_config_server_and_ACS(self, source_wrapper, converted_contents, asa_service): + def _convert_config_server_and_ACS(self, source_wrapper, converted_contents): enabled_config_server = False for config_server in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers'): diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 88993844f3f..6798fb3d3ec 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -9,7 +9,7 @@ def load_source(self): pass def calculate_data(self): - name = self.source['name'].split('/')[-1] + name = "eureka" configurations = [] replicas = 1 diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 6a888fc82f6..053c489d9c7 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -15,7 +15,7 @@ def load_source(self, source): self.source['secretEnvs'] = secret_envs_dict def calculate_data(self): - gatewayName = self.source['gateway']['name'].split('/')[-1] + gatewayName = f"gateway" configurations = self._get_configurations(self.source) replicas = min(2, self.source['gateway']['sku']['capacity']) routes = self._get_routes(self.source['routes']) @@ -52,7 +52,7 @@ def _get_routes(self, routes): "uri": r.get('uri', aca_uri), "predicates": r.get('predicates'), "filters": r.get('filters'), - "order": r.get('order') + "order": r.get('order') or 0, }) return aca_routes diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 2b8916587c7..9d4a9500415 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -9,7 +9,7 @@ def load_source(self, source): self.source = source def calculate_data(self): - name = self.source['name'].split('/')[-1] + name = "admin" configurations = [] replicas = 1 diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index a77874cfd43..0fd75257a05 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -14,8 +14,6 @@ def calculate_data(self): moduleName = appName.replace("-", "") templateName = f"{appName}_app.bicep" - print(f"appName: {appName}, moduleName: {moduleName}, templateName: {templateName}") - self.data["apps"].append({ "appName": appName, "moduleName": moduleName, diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index 442a4c80c30..9d0f38f9893 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -9,7 +9,7 @@ def load_source(self, source): self.source = source def calculate_data(self): - name = self.source['name'].split('/')[-1] + name = f"eureka" configurations = [] replicas = 1 diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index d8497da0d34..cc3c4a63b52 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -44,6 +44,14 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { } } ] + serviceBinds: [ + {% for bind in data.template.serviceBinds %} + { + name: '{{ bind.name }}' + serviceId: {{ bind.serviceId }} + }{% if not loop.last %}{% endif %} + {% endfor %} + ] scale: { minReplicas: minReplicas maxReplicas: maxReplicas 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 index 1bf7c79eff2..7d82eefed8a 100644 --- a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -1,10 +1,7 @@ param managedEnvironments_aca_env_name string -resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { +resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { name: '${managedEnvironments_aca_env_name}/{{ data.configServerName }}' - dependsOn: [ - resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) - ] properties: { componentType: 'SpringCloudConfig' configurations: [ @@ -12,7 +9,7 @@ resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024 { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %},{% endif %} + }{% if not loop.last %}{% endif %} {% endfor %} ] scale: { diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 4b1d19ffc7d..15276c9b6b3 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -1,12 +1,10 @@ // Params -param workloadProfileName string = 'Consumption' -param workloadProfileType string = 'Consumption' -param minNodes int = 1 -param maxNodes int = 4 +param workloadProfileName string +param workloadProfileType string param managedEnvName string = '{{data.containerAppEnvName}}' param containerAppLogAnalyticsName string = '{{data.containerAppLogAnalyticsName}}' -param zoneRedundant = '{{data.zoneRedundant}}' +param zoneRedundant bool = {{data.zoneRedundant}} resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: containerAppLogAnalyticsName @@ -36,8 +34,8 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { { name: workloadProfileName workloadProfileType: workloadProfileType - minimumCount: minNodes - maximumCount: maxNodes + // minimumCount: minNodes + // maximumCount: maxNodes } ] } @@ -46,12 +44,14 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { {% if data.scheduledEntries %} resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigurations@2024-10-02-Preview' = { name: '${managedEnvName}/default' - dependsOn: [ - resourceId('Microsoft.App/managedEnvironments', managedEnvName) - ] + parent: containerAppEnv location: resourceGroup().location properties: { - scheduledEntries: {{ data.scheduledEntries }} + scheduledEntries: [ + {% for entry in data.scheduledEntries %} + { weekDay: '{{ entry.weekDay }}', startHourUtc: {{ entry.startHourUtc }}, durationHours: {{ entry.durationHours }} }{{ "," if not loop.last }} + {% endfor %} + ] } } {% endif %} diff --git a/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 index 9125606bafe..e900a399365 100644 --- a/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 @@ -1,10 +1,7 @@ param managedEnvironments_aca_env_name string -resource eurekaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { +resource eurekaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { name: '${managedEnvironments_aca_env_name}/{{ data.eurekaName }}' - dependsOn: [ - resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) - ] properties: { componentType: 'SpringCloudEureka' configurations: [ @@ -12,7 +9,7 @@ resource eurekaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02 { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %},{% endif %} + }{% if not loop.last %}{% endif %} {% endfor %} ] scale: { diff --git a/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 index a844e132c16..bfb0c0bb8dc 100644 --- a/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 @@ -1,10 +1,7 @@ param managedEnvironments_aca_env_name string -resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { +resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { name: '${managedEnvironments_aca_env_name}/{{ data.gatewayName }}' - dependsOn: [ - resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) - ] properties: { componentType: 'SpringCloudGateway' configurations: [ @@ -12,7 +9,7 @@ resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-0 { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %},{% endif %} + }{% if not loop.last %}{% endif %} {% endfor %} ] scale: { @@ -27,15 +24,15 @@ resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-0 order: {{ route.order }} predicates: [ {% for predicate in route.predicates %} - '{{ predicate }}'{% if not loop.last %},{% endif %} + '{{ predicate }}'{% if not loop.last %}{% endif %} {% endfor %} ] filters: [ {% for filter in route.filters %} - '{{ filter }}'{% if not loop.last %},{% endif %} + '{{ filter }}'{% if not loop.last %}{% endif %} {% endfor %} ] - }{% if not loop.last %},{% endif %} + }{% if not loop.last %}{% endif %} {% endfor %} ] } diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 69625a50a98..f6495044586 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -1,22 +1,21 @@ // Params param workloadProfileName string param workloadProfileType string -param minNodes int -param maxNodes int module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' params: { workloadProfileName: workloadProfileName workloadProfileType: workloadProfileType - minNodes: minNodes - maxNodes: maxNodes } } {% for item in data.apps %} module {{ item.moduleName }} '{{ item.templateName }}' = { - name: '{{ item.appName }}' + name: '{{ item.appName }}Deployment' + dependsOn: [ + containerAppEnv + ] params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId } @@ -25,6 +24,9 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { module managedGateway 'gateway.bicep' = { name: 'gatewayDeployment' + dependsOn: [ + containerAppEnv + ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -32,6 +34,9 @@ module managedGateway 'gateway.bicep' = { module managedConfig 'config_server.bicep' = { name: 'configServerDeployment' + dependsOn: [ + containerAppEnv + ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -39,6 +44,9 @@ module managedConfig 'config_server.bicep' = { module managedEureka 'eureka.bicep' = { name: 'eurekaDeployment' + dependsOn: [ + containerAppEnv + ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -46,6 +54,9 @@ module managedEureka 'eureka.bicep' = { module managedSpringBootAdmin 'spring_boot_admin.bicep' = { name: 'springBootAdminDeployment' + dependsOn: [ + containerAppEnv + ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index a6e3d211a64..62c211dceb9 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -1,6 +1,4 @@ using './main.bicep' -param workloadProfileName = 'myprofile' -param workloadProfileType = 'D4' -param minNodes = 1 -param maxNodes = 2 \ No newline at end of file +param workloadProfileName = 'Consumption' +param workloadProfileType = 'Consumption' \ 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 index 7ff6fb0b987..9f7b3e5738b 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -4,51 +4,66 @@ This README provides instructions on how to use the Bicep files generated from y Before you begin, ensure you have the following: -1. Azure Subscription: An active Azure subscription. +1. Azure Subscription: An Azure subscription with sufficient permissions to create and manage resources for deploying Azure Container Apps. -2. Azure CLI: Installed and authenticated. You can download it here. +2. Bicep CLI: Ensure the Bicep CLI is installed. -3. Bicep CLI: Installed and available. Installation steps are available here. +3. Generated Bicep files: Make sure you have run the following command to generate the Bicep files from your Azure Spring Apps service to Azure Container Apps. -4. Permissions: Sufficient permissions to create and manage resources in your Azure subscription. +```azurecli +az spring migration-aca start --service --resource-group --subscription +``` -## Steps to Deploy Azure Container Apps Using Bicep Files +## Steps to Deploy Azure Container Apps Using the Bicep Files -1. Review and Customize Bicep Files +1. Review the Bicep Files and Modify Parameters -Open the Bicep files generated in the directory and review the parameters and resource definitions. Modify parameter values as needed to align with your environment (e.g., resource group name, location, application names). +After running the command to generate the Bicep files, you will have the following files and related resource definitions: -2. [Optional] Create a new Resource Group +- `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. -If the resource group specified in the Bicep files does not already exist, create one: +Replace the values in `param.bicepparam` to customize your Azure Container Apps. -az group create --name --location +2. [Optional] Customize the Bicep Files + +Modify values in the Bicep files as needed to align with your environment. + +- TODO: Add app containerization links and replace the image. +- If you are migrating Application Configuration Service or Config Server, you need to provide the credentials to the remote Git repository. Replace the "*" with the correct credentials in the file `config_server.bicep`. -Replace the "*" with the correct credentials in config server bicep file +3. [Optional] Create a new Resource Group for Azure Container Apps + +If the resource group you want to deploy the Azure Container Apps does not already exist, create one with the following command: + +```azurecli +az group create --name --location +``` 3. Deploy the Bicep Files Deploy the Bicep files using the Azure CLI: +```azurecli az deployment group create \ --resource-group \ --template-file main.bicep \ --parameters param.bicepparam - -Replace .json with the path to your parameter file. +``` 5. Validate Deployment -After deployment, validate that all resources, including the Azure Container Apps environment and applications, are correctly provisioned. You can check this in the Azure Portal or using the Azure CLI: +After deployment, validate that all resources, including the Azure Container Apps environment and applications, are correctly provisioned. You can check this in the Azure Portal: -az containerapp list --resource-group +- 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. Retry the deployment after fixing these issues. ## Post-Deployment Configuration Some properties and configurations are not included in the Bicep files and must be manually updated. Refer to the official Azure documentation for detailed steps: -### Networking - -### Scaling Settings: Configure scaling policies following Configure Scaling. - -## Known Limitations \ No newline at end of file +https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview 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 index 8e59a8e19d6..aa74b7e75c1 100644 --- 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 @@ -1,10 +1,7 @@ param managedEnvironments_aca_env_name string -resource sbaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-preview' = { +resource sbaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { name: '${managedEnvironments_aca_env_name}/{{ data.sbaName }}' - dependsOn: [ - resourceId('Microsoft.App/managedEnvironments', managedEnvironments_aca_env_name) - ] properties: { componentType: 'SpringBootAdmin' configurations: [ @@ -12,7 +9,7 @@ resource sbaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-pr { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %},{% endif %} + }{% if not loop.last %}{% endif %} {% endfor %} ] scale: { diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index bb23074f954..6c93034138c 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -4,7 +4,6 @@ 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 jinja2 import Environment, FileSystemLoader from .converter.conversion_context import ConversionContext from .converter.environment_converter import EnvironmentConverter from .converter.app_converter import AppConverter @@ -22,9 +21,9 @@ logger = get_logger(__name__) -def migration_aca_start(cmd, client, resource_group, service): - # API calls - logger.info("Start export ARM template for ASA service...") +def migration_aca_start(cmd, client, resource_group, service, output_folder): + logger.warning("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 @@ -42,9 +41,6 @@ def migration_aca_start(cmd, client, resource_group, service): context.add_converter(ReadMeConverter()) context.add_converter(ParamConverter()) - # Define the parameters for the Bicep template and output the Bicep files - logger.info("Start to generate ACA bicep files based on parameters...") - # Prepare bicep parameters main_bicep_params = get_aca_bicep_params(asa_arm) @@ -52,12 +48,13 @@ def migration_aca_start(cmd, client, resource_group, service): context.set_params_for_converter(EnvironmentConverter, main_bicep_params) # Run all converters - logger.info("Start to run all converters...") + logger.warning("Converting resources to Azure Container Apps...") converted_contents = context.run_converters(asa_arm) + 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, os.path.join("output","")) - logger.info("Succeed to generate Bicep files") + 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): diff --git a/src/spring/azext_spring/spring_instance.py b/src/spring/azext_spring/spring_instance.py index fbb460bb659..0e709ede604 100644 --- a/src/spring/azext_spring/spring_instance.py +++ b/src/spring/azext_spring/spring_instance.py @@ -327,5 +327,5 @@ def spring_private_dns_zone_clean(cmd, client, resource_group, service): resource_group_name=resource_group, service_name=service, resource=updated_resource) -def spring_migration_aca_start(cmd, client, resource_group, service): - migration_aca_start(cmd, client, resource_group, service) +def spring_migration_aca_start(cmd, client, resource_group, service, output_folder=None): + migration_aca_start(cmd, client, resource_group, service, output_folder) From e4b55880030eee81f7f40a7fee6c38cc49f3419d Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Feb 2025 13:53:50 +0800 Subject: [PATCH 16/98] npe --- .../converter/config_server_converter.py | 44 +++++++++---------- .../migration/converter/gateway_converter.py | 41 +++++++++-------- .../converter/templates/readme.md.j2 | 5 +-- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index 61a25991310..d5200848336 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -37,29 +37,29 @@ def get_template_name(self): def _get_configurations(self, source): configurations = [] - git_property = source['properties']['configServer']['gitProperty'] - git_repos = git_property['repositories'] + git_property = source.get('properties', {}).get('configServer', {}).get('gitProperty') + if git_property is not None: + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, git_property.get('uri')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, git_property.get('label')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, git_property.get('searchPaths')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, git_property.get('username')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, git_property.get('password')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, git_property.get('privateKey')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, git_property.get('hostKey')) + self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, git_property.get('hostKeyAlgorithm')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, git_property.get('uri')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, git_property.get('label')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, git_property.get('searchPaths')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, git_property.get('username')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, git_property.get('password')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, git_property.get('privateKey')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, git_property.get('hostKey')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, git_property.get('hostKeyAlgorithm')) - - for repo in git_repos: - configuration_key_repo_prefix = self.CONFIGURATION_KEY_PREFIX + ".repos." + repo['name'] - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PATTERN, repo.get('pattern')) + 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_existss(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm')) + self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PATTERN, repo.get('pattern')) return configurations diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 053c489d9c7..d344e91ccd0 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -29,16 +29,18 @@ def calculate_data(self): def _get_configurations(self, source): configurations = [] - for key, value in source['gateway']['properties']['environmentVariables']['properties'].items(): - configurations.append({ - "propertyName": key, - "value": value, - }) - for key, value in source['secretEnvs'].items(): - configurations.append({ - "propertyName": key, - "value": value, - }) + if source.get('gateway', {}).get('properties', {}).get('environmentVariables', {}).get('properties') is not None: + for key, value in source['gateway']['properties']['environmentVariables']['properties'].items(): + configurations.append({ + "propertyName": key, + "value": value, + }) + if source.get('secretEnvs') is not None: + for key, value in source['secretEnvs'].items(): + configurations.append({ + "propertyName": key, + "value": value, + }) return configurations def _get_routes(self, routes): @@ -46,18 +48,19 @@ def _get_routes(self, routes): for route in routes: aca_id = route['name'].split('/')[-1] aca_uri = self._get_uri_from_route(route) - for r in route['properties']['routes']: - aca_routes.append({ - "id": aca_id, - "uri": r.get('uri', aca_uri), - "predicates": r.get('predicates'), - "filters": r.get('filters'), - "order": r.get('order') or 0, - }) + if route.get('properties', {}).get('routes') is not None: + for r in route['properties']['routes']: + aca_routes.append({ + "id": aca_id, + "uri": r.get('uri', aca_uri), + "predicates": r.get('predicates'), + "filters": r.get('filters'), + "order": r.get('order') or 0, + }) return aca_routes def _get_uri_from_route(self, route): - app_resource_id = route['properties']['appResourceId'] + 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}" diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index 9f7b3e5738b..bd875426bc8 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -48,10 +48,7 @@ az group create --name --location Deploy the Bicep files using the Azure CLI: ```azurecli -az deployment group create \ - --resource-group \ - --template-file main.bicep \ - --parameters param.bicepparam +az deployment group create --resource-group --template-file main.bicep --parameters param.bicepparam ``` 5. Validate Deployment From 81fc28a93834119a61aa250dab64b266bb5185a1 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Feb 2025 16:54:50 +0800 Subject: [PATCH 17/98] app bind --- .../migration/converter/app_converter.py | 14 +---- .../migration/converter/conversion_context.py | 52 +++++++++++++------ .../migration/converter/main_converter.py | 10 ++-- .../converter/templates/environment.bicep.j2 | 2 +- .../converter/templates/main.bicep.j2 | 10 +++- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 919e5bd3569..ce3a73b4751 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -6,16 +6,14 @@ class AppConverter(ConverterTemplate): def load_source(self, source): self.source = source - self.source['enabled_sba'] = False # print(f"App source: {self.source}") def calculate_data(self): - enable_sba = False appName = self.source['name'].split('/')[-1] envName = self.source['name'].split('/')[0] moduleName = appName.replace("-", "") containers = [] - serviceBinds, dependsOn = self._get_service_bind(self.source, envName) + serviceBinds = self._get_service_bind(self.source, envName) self.data = { "containerAppName": appName, @@ -25,7 +23,6 @@ def calculate_data(self): "cpuCore": "0.5", "memorySize": "1", "minReplicas": 1, - "dependsOn": dependsOn, "template": { "containers": containers, "serviceBinds": serviceBinds, @@ -40,10 +37,7 @@ def get_app_name(input_string): return input_string.split('/')[-1] def _get_service_bind(self, source, envName): - enable_config = False - enable_eureka = False enable_sba = source['enabled_sba'] - dependsOn = [] service_bind = [] addon = source['properties'].get('addonConfigs') @@ -56,20 +50,16 @@ def _get_service_bind(self, source, envName): "name": "bind-config", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" }) - enable_config = True if addon.get('serviceRegistry') is not None and addon['serviceRegistry'].get('resourceId') is not None: service_bind.append({ "name": "bind-eureka", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" }) - enable_eureka = True if enable_sba: service_bind.append({ "name": "bind-sba", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'admin')" }) - if enable_config: - dependsOn.append("managedConfig") # print(f"Service bind: {service_bind}") - return service_bind, dependsOn + return service_bind diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index ed44feb17ef..e2b4eeb7ed5 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -40,19 +40,13 @@ def set_params_for_converter(self, converter_type, params): def run_converters(self, source): converted_contents = {} source_wrapper = SourceDataWrapper(source) - converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert( - source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - ) + converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert( source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] ) asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] - for app in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps'): - appName = app['name'].split('/')[-1] - converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) - # converted_contents.append( # self.get_converter(RevisionConverter).convert( # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') @@ -61,11 +55,31 @@ def run_converters(self, source): converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(None) converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) - converted_contents = self._convert_gateway(source_wrapper, converted_contents) - converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents) - converted_contents = self._convert_live_view(source_wrapper, converted_contents) - converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service) + managed_components = { + 'gateway': False, + 'config': False, + 'eureka': False, + 'sba': False, + } + converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) + converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, managed_components) + converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) + converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) + + asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + for app in asa_apps: + appName = app['name'].split('/')[-1] + app['enabled_sba'] = managed_components['sba'] + converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) + + main_source = { + "apps": asa_apps, + "managedComponents": managed_components, + } + converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert( + main_source + ) return converted_contents def save_to_files(self, converted_contents, output_path): @@ -78,8 +92,9 @@ def save_to_files(self, converted_contents, output_path): logger.info(f"Generating the file {output_filename}...") output_file.write(content) - def _convert_gateway(self, source_wrapper, converted_contents): + def _convert_gateway(self, source_wrapper, converted_contents, managed_components): for gateway in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways'): + managed_components['gateway'] = True gateway_key = self.get_converter(GatewayConverter).get_template_name() routes = [] for gateway_route in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways/routeConfigs'): @@ -92,10 +107,11 @@ def _convert_gateway(self, source_wrapper, converted_contents): logger.info(f"converted_contents for gateway: {converted_contents[gateway_key]}") return converted_contents - def _convert_config_server_and_ACS(self, source_wrapper, converted_contents): + def _convert_config_server_and_ACS(self, source_wrapper, converted_contents, managed_components): enabled_config_server = False for config_server in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers'): + managed_components['config'] = True enabled_config_server = True config_key = self.get_converter(ConfigServerConverter).get_template_name() converted_contents[config_key] = self.get_converter(ConfigServerConverter).convert(config_server) @@ -103,31 +119,35 @@ def _convert_config_server_and_ACS(self, source_wrapper, converted_contents): if not enabled_config_server: for acs in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices'): + managed_components['config'] = True config_key = self.get_converter(ACSConverter).get_template_name() converted_contents[config_key] = self.get_converter(ACSConverter).convert(acs) logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[config_key]}") return converted_contents - def _convert_live_view(self, source_wrapper, converted_contents): + def _convert_live_view(self, source_wrapper, converted_contents, managed_components): for live_view in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews'): + managed_components['sba'] = True live_view_key = self.get_converter(LiveViewConverter).get_template_name() converted_contents[live_view_key] = self.get_converter(LiveViewConverter).convert(live_view) logger.info(f"converted_contents for Live View: {converted_contents[live_view_key]}") return converted_contents - def _convert_eureka_and_service_registry(self, source_wrapper, converted_contents, asa_service): + def _convert_eureka_and_service_registry(self, source_wrapper, converted_contents, asa_service, managed_components): is_enterprise_tier = self._is_enterprise_tier(asa_service) for service_registry in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries'): + managed_components['eureka'] = True eureka_key = self.get_converter(ServiceRegistryConverter).get_template_name() converted_contents[eureka_key] = self.get_converter(ServiceRegistryConverter).convert(service_registry) logger.info(f"converted_contents for Service Registry: {converted_contents[eureka_key]}") return converted_contents if not is_enterprise_tier: + managed_components['eureka'] = True eureka_key = self.get_converter(EurekaConverter).get_template_name() converted_contents[eureka_key] = self.get_converter(EurekaConverter).convert() - return converted_contents + return converted_contents def _is_enterprise_tier(self, asa_service): return asa_service['sku']['tier'] == 'Enterprise' diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 0fd75257a05..301b6376802 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -3,13 +3,13 @@ # Concrete Converter Subclass for Read Me class MainConverter(ConverterTemplate): def load_source(self, source): - self.source = [] - for resource in source: - self.source.append(resource) + self.source = source + self.apps = source["apps"] + self.managedComponents = source["managedComponents"] def calculate_data(self): self.data.setdefault("apps", []) - for item in self.source: + for item in self.apps: appName = item['name'].split('/')[-1] moduleName = appName.replace("-", "") templateName = f"{appName}_app.bicep" @@ -19,6 +19,8 @@ def calculate_data(self): "moduleName": moduleName, "templateName": templateName, }) + for name, value in self.managedComponents.items(): + self.data[name] = value def get_template_name(self): return "main.bicep" \ 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 index 15276c9b6b3..f808088fa4f 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -43,7 +43,7 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { {% if data.scheduledEntries %} resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigurations@2024-10-02-Preview' = { - name: '${managedEnvName}/default' + name: 'default' parent: containerAppEnv location: resourceGroup().location properties: { diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index f6495044586..fdff97448f9 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -22,6 +22,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { } {% endfor %} +{% if data.gateway == true %} module managedGateway 'gateway.bicep' = { name: 'gatewayDeployment' dependsOn: [ @@ -31,7 +32,9 @@ module managedGateway 'gateway.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } +{% endif %} +{% if data.config == true %} module managedConfig 'config_server.bicep' = { name: 'configServerDeployment' dependsOn: [ @@ -41,7 +44,9 @@ module managedConfig 'config_server.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } +{% endif %} +{% if data.eureka == true %} module managedEureka 'eureka.bicep' = { name: 'eurekaDeployment' dependsOn: [ @@ -51,7 +56,9 @@ module managedEureka 'eureka.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } +{% endif %} +{% if data.sba == true %} module managedSpringBootAdmin 'spring_boot_admin.bicep' = { name: 'springBootAdminDeployment' dependsOn: [ @@ -60,4 +67,5 @@ module managedSpringBootAdmin 'spring_boot_admin.bicep' = { params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } -} \ No newline at end of file +} +{% endif %} \ No newline at end of file From 6bba3bd3ea03f77ef1f5df2b82878c10ad84a0a8 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Tue, 18 Feb 2025 20:01:41 +0800 Subject: [PATCH 18/98] Add key vault cert migration --- .../migration/converter/app_converter.py | 2 +- .../migration/converter/cert_converter.py | 43 +++++++++++++++++++ .../migration/converter/conversion_context.py | 13 +++++- .../converter/environment_converter.py | 2 + .../migration/converter/main_converter.py | 22 ++++++++-- .../converter/templates/cert.bicep.j2 | 19 ++++++++ .../converter/templates/environment.bicep.j2 | 5 ++- .../converter/templates/main.bicep.j2 | 14 +++++- .../converter/templates/readme.md.j2 | 15 +++---- .../migration/migration_operations.py | 2 + 10 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 src/spring/azext_spring/migration/converter/cert_converter.py create mode 100644 src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index ce3a73b4751..50db46fad4e 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -11,7 +11,7 @@ def load_source(self, source): def calculate_data(self): appName = self.source['name'].split('/')[-1] envName = self.source['name'].split('/')[0] - moduleName = appName.replace("-", "") + moduleName = appName.replace("-", "_") containers = [] serviceBinds = self._get_service_bind(self.source, envName) 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..9f141eb0ab6 --- /dev/null +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -0,0 +1,43 @@ +import re + +from .base_converter import ConverterTemplate + +# Concrete Converter Subclass for certificate +class CertConverter(ConverterTemplate): + def load_source(self, source): + self.source = source + print(f"Cert source: {self.source}") + + def calculate_data(self): + certName = self.source['name'].split('/')[-1] + moduleName = "cert_" + certName.replace("-", "_") + isKeyVaultCert = False + certKeyVault = self._get_cert_key_vault() + + self.data = { + "certName": certName, + "moduleName": moduleName, + "certificateType": "ServerSSLCertificate", + } + + if certKeyVault: + self.data["certificateKeyVaultProperties"] = certKeyVault + isKeyVaultCert = True + else: + self.data["value"] = "*" + isKeyVaultCert = False + self.data["isKeyVaultCert"] = isKeyVaultCert + print(f"cert data: {self.data}") + + def get_template_name(self): + return "cert.bicep" + + def _get_cert_key_vault(self): + certKeyVault = None + if self.source['properties'].get('type') == "KeyVaultCertificate": + if self.source['properties'].get('vaultUri') and self.source['properties'].get('keyVaultCertName'): + certKeyVault = { + "keyVaultUrl": self.source['properties']['vaultUri'] + "/secrets/" + self.source['properties']['keyVaultCertName'], + "identity": "system" + } + return certKeyVault diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index e2b4eeb7ed5..1e93a9f6ed7 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -15,6 +15,7 @@ from .config_server_converter import ConfigServerConverter from .acs_converter import ACSConverter from .live_view_converter import LiveViewConverter +from .cert_converter import CertConverter logger = get_logger(__name__) @@ -40,12 +41,19 @@ def set_params_for_converter(self, converter_type, params): def run_converters(self, source): converted_contents = {} source_wrapper = SourceDataWrapper(source) + asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert( - source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + asa_service ) + asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') + asa_kv_certs = [] - asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + for cert in asa_certs: + certName = cert['name'].split('/')[-1] + if cert['properties'].get('type') == "KeyVaultCertificate": + asa_kv_certs.append(cert) + converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) # converted_contents.append( # self.get_converter(RevisionConverter).convert( @@ -75,6 +83,7 @@ def run_converters(self, source): main_source = { "apps": asa_apps, + "certs": asa_kv_certs, "managedComponents": managed_components, } converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert( diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 67524a5131b..5f922a006e0 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -25,5 +25,7 @@ def calculate_data(self): }] self.data["scheduledEntries"] = aca_maintenance_window + asa_certs = self.source['properties'].get('certificates') + def get_template_name(self): return "environment.bicep" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 301b6376802..3610a0bfa21 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -6,19 +6,33 @@ def load_source(self, source): self.source = source self.apps = source["apps"] self.managedComponents = source["managedComponents"] + self.certs = source["certs"] def calculate_data(self): + self.data.setdefault("certs", []) + for item in self.certs: + certName = item['name'].split('/')[-1] + moduleName = "cert_" + certName.replace("-", "_") + templateName = f"{certName}_cert.bicep" + certData = { + "certName": certName, + "moduleName": moduleName, + "templateName": templateName, + } + self.data["certs"].append(certData) + self.data.setdefault("apps", []) for item in self.apps: appName = item['name'].split('/')[-1] - moduleName = appName.replace("-", "") + moduleName = appName.replace("-", "_") templateName = f"{appName}_app.bicep" - - self.data["apps"].append({ + appData = { "appName": appName, "moduleName": moduleName, "templateName": templateName, - }) + } + self.data["apps"].append(appData) + for name, value in self.managedComponents.items(): self.data[name] = value 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..82a6ee84e26 --- /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/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index f808088fa4f..7629cbbd47d 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -16,9 +16,12 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { } } -resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { +resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { name: managedEnvName location: resourceGroup().location + identity: { + type: 'SystemAssigned' + } properties: { // vnetConfiguration: {} appLogsConfiguration: { diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index fdff97448f9..84d4f33a425 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -10,6 +10,18 @@ module containerAppEnv 'environment.bicep' = { } } +{% for item in data.certs %} +module {{ item.moduleName }} '{{ item.templateName }}' = { + name: 'Cert{{ item.certName }}Deployment' + dependsOn: [ + containerAppEnv + ] + params: { + managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName + } +} +{% endfor %} + {% for item in data.apps %} module {{ item.moduleName }} '{{ item.templateName }}' = { name: '{{ item.appName }}Deployment' @@ -68,4 +80,4 @@ module managedSpringBootAdmin 'spring_boot_admin.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } -{% endif %} \ No newline at end of file +{% endif %} diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index bd875426bc8..1d8ec211964 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -16,7 +16,7 @@ az spring migration-aca start --service --resou ## Steps to Deploy Azure Container Apps Using the Bicep Files -1. Review the Bicep Files and Modify Parameters +###. Review the Bicep Files and Modify Parameters After running the command to generate the Bicep files, you will have the following files and related resource definitions: @@ -28,30 +28,29 @@ After running the command to generate the Bicep files, you will have the followi Replace the values in `param.bicepparam` to customize your Azure Container Apps. -2. [Optional] Customize the Bicep Files +###. Customize the Bicep Files Modify values in the Bicep files as needed to align with your environment. - TODO: Add app containerization links and replace the image. - If you are migrating Application Configuration Service or Config Server, you need to provide the credentials to the remote Git repository. Replace the "*" with the correct credentials in the file `config_server.bicep`. -3. [Optional] Create a new Resource Group for Azure Container Apps -If the resource group you want to deploy the Azure Container Apps does not already exist, create one with the following command: +### Create a new Resource Group for Azure Container Apps + +#### If the resource group you want to deploy the Azure Container Apps does not already exist, create one with the following command: ```azurecli az group create --name --location ``` -3. Deploy the Bicep Files - -Deploy the Bicep files using the Azure CLI: +#### Deploy the Bicep files using the Azure CLI: ```azurecli az deployment group create --resource-group --template-file main.bicep --parameters param.bicepparam ``` -5. Validate Deployment +#### Validate Deployment After deployment, validate that all resources, including the Azure Container Apps environment and applications, are correctly provisioned. You can check this in the Azure Portal: diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 6c93034138c..52d198c8cf6 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -17,6 +17,7 @@ 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__) @@ -40,6 +41,7 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): context.add_converter(LiveViewConverter()) context.add_converter(ReadMeConverter()) context.add_converter(ParamConverter()) + context.add_converter(CertConverter()) # Prepare bicep parameters main_bicep_params = get_aca_bicep_params(asa_arm) From 90df2c89b8a4cc849780552f02ee25d2866ccf31 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Wed, 19 Feb 2025 14:16:58 +0800 Subject: [PATCH 19/98] convert deployment logic --- .../migration/converter/app_converter.py | 128 ++++++++++++++++-- .../migration/converter/conversion_context.py | 14 +- .../migration/converter/main_converter.py | 1 + .../migration/converter/param_converter.py | 10 +- .../migration/converter/revision_converter.py | 23 ---- .../converter/templates/app.bicep.j2 | 89 ++++++++++-- .../converter/templates/main.bicep.j2 | 4 + .../converter/templates/param.bicepparam.j2 | 6 +- .../converter/templates/revision.bicep.j2 | 26 ---- .../migration/migration_operations.py | 2 - 10 files changed, 223 insertions(+), 80 deletions(-) delete mode 100644 src/spring/azext_spring/migration/converter/revision_converter.py delete mode 100644 src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index ce3a73b4751..c9553d02fcd 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -12,22 +12,22 @@ def calculate_data(self): appName = self.source['name'].split('/')[-1] envName = self.source['name'].split('/')[0] moduleName = appName.replace("-", "") - containers = [] serviceBinds = self._get_service_bind(self.source, envName) + deployments = self._get_deployments(self.source) + blueDeployment = deployments[0] if len(deployments) > 0 else {} + greenDeployment = deployments[1] if len(deployments) > 1 else {} self.data = { "containerAppName": appName, + "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), "moduleName": moduleName, - "containerImage": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", "targetPort": "80", - "cpuCore": "0.5", - "memorySize": "1", "minReplicas": 1, - "template": { - "containers": containers, - "serviceBinds": serviceBinds, - }, - "maxReplicas": 5 + "maxReplicas": 5, + "serviceBinds": serviceBinds, + "blue": blueDeployment, + "green": greenDeployment, + "isBlueGreen": len(deployments) > 1 } def get_template_name(self): @@ -62,4 +62,114 @@ def _get_service_bind(self, source, envName): }) # print(f"Service bind: {service_bind}") return service_bind + + def _get_deployments(self, source): + deployments = [] + for deployment in source['deployments']: + deployment_name = deployment['name'].split('/')[-1] + 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', {}) + 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') + deployment = { + "name": deployment_name, + "env": [ + { + "name": key, + "value": value + } for key, value in env.items() + ], + "livenessProbe": self._convert_probe(liveness_probe, tier), + "readinessProbe": self._convert_probe(readiness_probe, tier), + "cpuCore": cpuCore, + "memorySize": self._get_memory_by_cpu(cpuCore) or memorySize, + } + deployments.append(deployment) + + # print(f"deployments: {deployments}") + return deployments + + # 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): + 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" + } + 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): + print(f"probe: {probe}") + if probe is None: + return None + if probe.get("disableProbe") == True: + print(f"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']. + 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 + return result + + 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 + print(f"probeAction: {probeAction}") + 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 + print(f"probeAction: {probeAction}") + return probeAction + diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index e2b4eeb7ed5..f9acd120852 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -5,7 +5,6 @@ from .base_converter import ConverterTemplate from .environment_converter import EnvironmentConverter from .app_converter import AppConverter -from .revision_converter import RevisionConverter from .readme_converter import ReadMeConverter from .main_converter import MainConverter from .param_converter import ParamConverter @@ -47,14 +46,6 @@ def run_converters(self, source): asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] - # converted_contents.append( - # self.get_converter(RevisionConverter).convert( - # source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') - # ) - # ) - converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(None) - converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) - managed_components = { 'gateway': False, 'config': False, @@ -67,16 +58,21 @@ def run_converters(self, source): converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + asa_deployments = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') + # print(asa_deployments) for app in asa_apps: appName = app['name'].split('/')[-1] app['enabled_sba'] = managed_components['sba'] + app['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app['name']}/")] converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) main_source = { "apps": asa_apps, "managedComponents": managed_components, } + converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(asa_apps) + converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert( main_source ) diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 301b6376802..55ff5f7cdfa 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -18,6 +18,7 @@ def calculate_data(self): "appName": appName, "moduleName": moduleName, "templateName": templateName, + "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), }) for name, value in self.managedComponents.items(): self.data[name] = value diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index a6c7a58993e..01e2e16491c 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -3,10 +3,16 @@ # Concrete Converter Subclass for paramter class ParamConverter(ConverterTemplate): def load_source(self, source): - pass + self.apps = source def calculate_data(self): - pass + self.data.setdefault("apps", []) + for item in self.apps: + appName = item['name'].split('/')[-1] + self.data["apps"].append({ + "appName": appName, + "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + }) def get_template_name(self): return "param.bicepparam" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/revision_converter.py b/src/spring/azext_spring/migration/converter/revision_converter.py deleted file mode 100644 index 21c692315a2..00000000000 --- a/src/spring/azext_spring/migration/converter/revision_converter.py +++ /dev/null @@ -1,23 +0,0 @@ -from .base_converter import ConverterTemplate - -# Concrete Converter Subclass for Revision -class RevisionConverter(ConverterTemplate): - def load_source(self, source): - self.source = [] - for resource in source: - self.source.append(resource) - - def calculate_data(self): - self.data.revisions = [] - for revision in self.source: - self.data.revisions.append({ - "name": revision["properties"]["name"], - "app": self.source.app_name, - "cpu_core": "0.5", - "memory_size": "1", - "min_replicas": 1, - "max_replicas": 5, - }) - - def get_template_name(self): - return "revision.bicep" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index cc3c4a63b52..d15b5853344 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -1,10 +1,7 @@ param containerAppName string = '{{data.containerAppName}}' -param containerImage string = '{{data.containerImage}}' +param {{data.containerAppImageName}} string param targetPort int = {{data.targetPort}} -param cpuCore string = '{{data.cpuCore}}' -param memorySize string = '{{data.memorySize}}' - @description('Minimum number of replicas that will be deployed') @minValue(0) param minReplicas int = {{data.minReplicas}} @@ -32,20 +29,96 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { } ] } + activeRevisionsMode: '{% if data.isBlueGreen %}Multiple{% else %}Single{% endif %}' } template: { + // revisionSuffix: '{{ data.blue.name }}' containers: [ { name: containerAppName - image: containerImage + image: {{data.containerAppImageName}} resources: { - cpu: json(cpuCore) - memory: '${memorySize}Gi' + cpu: json('{{data.blue.cpuCore}}') + memory: '{{data.blue.memorySize}}' } + env: [ + {% for env in data.blue.env %} + { + name: '{{ env.name }}' + value: '{{ env.value }}' + }{% if not loop.last %}{% endif %} + {% endfor %} + ] + 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.blue.livenessProbe.httpGet.port}} + scheme: '{{data.blue.livenessProbe.httpGet.scheme}}' + } + {%- endif %} + {% if data.blue.livenessProbe.tcpSocket -%} + tcpSocket: { + port: {{data.blue.livenessProbe.tcpSocket.port}} + } + {%- 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.blue.readinessProbe.httpGet.port}} + scheme: '{{data.blue.readinessProbe.httpGet.scheme}}' + } + {%- endif %} + {% if data.blue.readinessProbe.tcpSocket -%} + tcpSocket: { + port: {{data.blue.readinessProbe.tcpSocket.port}} + } + {%- 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 %} + ] } ] serviceBinds: [ - {% for bind in data.template.serviceBinds %} + {% for bind in data.serviceBinds %} { name: '{{ bind.name }}' serviceId: {{ bind.serviceId }} diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index fdff97448f9..9c6cdaae36e 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -1,6 +1,9 @@ // Params param workloadProfileName string param workloadProfileType string +{% for app in data.apps %} +param {{app.containerAppImageName}} string +{% endfor %} module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' @@ -18,6 +21,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { ] params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId + {{item.containerAppImageName}}: {{item.containerAppImageName}} } } {% endfor %} diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 62c211dceb9..61dcda140b4 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -1,4 +1,8 @@ using './main.bicep' param workloadProfileName = 'Consumption' -param workloadProfileType = 'Consumption' \ No newline at end of file +param workloadProfileType = 'Consumption' + +{% for app in data.apps %} +param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' +{% endfor %} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 deleted file mode 100644 index aa7e2c9d349..00000000000 --- a/src/spring/azext_spring/migration/converter/templates/revision.bicep.j2 +++ /dev/null @@ -1,26 +0,0 @@ -param containerAppName string = {{containerAppName}} -param containerImage string = {{containerImage}} - -param cpuCore string = {{cpuCore}} -param memorySize string = {{memorySize}} - -param containerAppEnvId string - -resource conainerAppRevision 'Microsoft.App/containerApp/revisions@2024-03-01' = [for revision in range(1, 2): { - name: '${containerAppName}-${revision}' - parent: containerApp - properties: { - template: { - containers: [ - { - name: containerAppName - image: containerImage - resources: { - cpu: json(cpuCore) - memory: '${memorySize}Gi' - } - } - ] - } - } -}] diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 6c93034138c..d90bed128d8 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -7,7 +7,6 @@ from .converter.conversion_context import ConversionContext from .converter.environment_converter import EnvironmentConverter from .converter.app_converter import AppConverter -from .converter.revision_converter import RevisionConverter from .converter.gateway_converter import GatewayConverter from .converter.eureka_converter import EurekaConverter from .converter.service_registry_converter import ServiceRegistryConverter @@ -31,7 +30,6 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): context.add_converter(MainConverter()) context.add_converter(EnvironmentConverter()) context.add_converter(AppConverter()) - context.add_converter(RevisionConverter()) context.add_converter(GatewayConverter(client, resource_group, service)) context.add_converter(EurekaConverter()) context.add_converter(ServiceRegistryConverter()) From 9c0604a03d34035ecc229ad1d92466fad29c1aad Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Wed, 19 Feb 2025 16:57:33 +0800 Subject: [PATCH 20/98] adjust log as azure monitor --- .../converter/templates/environment.bicep.j2 | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 7629cbbd47d..e02bc23f73b 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -16,6 +16,20 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { } } +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: managedEnvName location: resourceGroup().location @@ -25,11 +39,7 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' properties: { // vnetConfiguration: {} appLogsConfiguration: { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: logAnalytics.properties.customerId - sharedKey: logAnalytics.listKeys().primarySharedKey - } + destination: 'azure-monitor' } zoneRedundant: zoneRedundant // customDomainConfiguration: {} From 18b9d614599e5f7beecc91469ebc836a56367843 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Wed, 19 Feb 2025 17:30:22 +0800 Subject: [PATCH 21/98] disable service bniding --- .../migration/converter/app_converter.py | 4 ++-- .../migration/converter/conversion_context.py | 24 +++++++++---------- .../migration/converter/main_converter.py | 6 ++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 485ceab6b8a..1d1a5166457 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -12,7 +12,7 @@ def calculate_data(self): appName = self.source['name'].split('/')[-1] envName = self.source['name'].split('/')[0] moduleName = appName.replace("-", "_") - serviceBinds = self._get_service_bind(self.source, envName) + # serviceBinds = self._get_service_bind(self.source, envName) deployments = self._get_deployments(self.source) blueDeployment = deployments[0] if len(deployments) > 0 else {} greenDeployment = deployments[1] if len(deployments) > 1 else {} @@ -24,7 +24,7 @@ def calculate_data(self): "targetPort": "80", "minReplicas": 1, "maxReplicas": 5, - "serviceBinds": serviceBinds, + # "serviceBinds": serviceBinds, "blue": blueDeployment, "green": greenDeployment, "isBlueGreen": len(deployments) > 1 diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index c4813373876..f94327376ac 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -54,16 +54,16 @@ def run_converters(self, source): asa_kv_certs.append(cert) converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) - managed_components = { - 'gateway': False, - 'config': False, - 'eureka': False, - 'sba': False, - } - converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) - converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, managed_components) - converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) - converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) + # managed_components = { + # 'gateway': False, + # 'config': False, + # 'eureka': False, + # 'sba': False, + # } + # converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) + # converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, managed_components) + # converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) + # converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') asa_deployments = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') @@ -71,14 +71,14 @@ def run_converters(self, source): for app in asa_apps: appName = app['name'].split('/')[-1] - app['enabled_sba'] = managed_components['sba'] + # app['enabled_sba'] = managed_components['sba'] app['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app['name']}/")] converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) main_source = { "apps": asa_apps, "certs": asa_kv_certs, - "managedComponents": managed_components, + # "managedComponents": managed_components, } converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(asa_apps) converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index b227836caf4..49b7628cb9b 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -5,7 +5,7 @@ class MainConverter(ConverterTemplate): def load_source(self, source): self.source = source self.apps = source["apps"] - self.managedComponents = source["managedComponents"] + # self.managedComponents = source["managedComponents"] self.certs = source["certs"] def calculate_data(self): @@ -34,8 +34,8 @@ def calculate_data(self): } self.data["apps"].append(appData) - for name, value in self.managedComponents.items(): - self.data[name] = value + # for name, value in self.managedComponents.items(): + # self.data[name] = value def get_template_name(self): return "main.bicep" \ No newline at end of file From 23626062bb8a5d713aba7b5731df8b6ed14a298d Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Wed, 19 Feb 2025 22:54:12 +0800 Subject: [PATCH 22/98] ingress --- .../migration/converter/app_converter.py | 42 +++++++++++-------- .../converter/templates/app.bicep.j2 | 22 ++++++---- .../converter/templates/main.bicep.j2 | 4 +- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 1d1a5166457..e3bd3390b5b 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -16,12 +16,16 @@ def calculate_data(self): deployments = self._get_deployments(self.source) blueDeployment = deployments[0] if len(deployments) > 0 else {} greenDeployment = deployments[1] if len(deployments) > 1 else {} + tier = blueDeployment.get('sku', {}).get('tier') + ingress = self._get_ingress(self.source, tier) + isPublic = self.source['properties'].get('public') self.data = { "containerAppName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), "moduleName": moduleName, - "targetPort": "80", + "ingress": ingress, + "isPublic": isPublic, "minReplicas": 1, "maxReplicas": 5, # "serviceBinds": serviceBinds, @@ -117,30 +121,23 @@ def _get_memory_by_cpu(self, cpu): # create a method _convert_probe to convert the probe from the source to the target format def _convert_probe(self, probe, tier): - print(f"probe: {probe}") + # print(f"probe: {probe}") if probe is None: return None if probe.get("disableProbe") == True: print(f"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']. 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 + result = { + "initialDelaySeconds": initialDelaySeconds, + "periodSeconds": probe.get("periodSeconds", None), + "timeoutSeconds": probe.get("timeoutSeconds", None), + "successThreshold": probe.get("successThreshold", None), + "failureThreshold": probe.get("failureThreshold", None), + } httpGet = self._convert_http_probe_action(probe, tier) if httpGet is not None: result["httpGet"] = httpGet @@ -157,7 +154,7 @@ def _convert_tcp_probe_action(self, probe, tier): } else: probeAction = None - print(f"probeAction: {probeAction}") + # print(f"probeAction: {probeAction}") return probeAction def _convert_http_probe_action(self, probe, tier): @@ -170,6 +167,15 @@ def _convert_http_probe_action(self, probe, tier): } else: probeAction = None - print(f"probeAction: {probeAction}") + # print(f"probeAction: {probeAction}") return probeAction + def _get_ingress(self, source, tier): + ingress = source['properties'].get('ingressSettings') + if ingress is None: + return None + return { + "targetPort": 8080 if tier == "Enterprise" else 1025, + "transport": ingress.get('backendProtocol').replace("Default", "auto"), + "sessionAffinity": ingress.get('sessionAffinity').replace("Cookie", "sticky").replace("None", "none") + } diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index d15b5853344..d808c54ba94 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -1,6 +1,5 @@ param containerAppName string = '{{data.containerAppName}}' param {{data.containerAppImageName}} string -param targetPort int = {{data.targetPort}} @description('Minimum number of replicas that will be deployed') @minValue(0) @@ -19,15 +18,24 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { managedEnvironmentId: containerAppEnvId configuration: { ingress: { - external: true - targetPort: targetPort + {% if data.ingress -%} + external: {% if data.isPublic %}true{% else %}false{% endif %} + targetPort: {{data.ingress.targetPort}} 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 %}' } @@ -42,7 +50,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { memory: '{{data.blue.memorySize}}' } env: [ - {% for env in data.blue.env %} + {% for env in data.blue.env -%} { name: '{{ env.name }}' value: '{{ env.value }}' @@ -61,7 +69,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { port: {{data.blue.livenessProbe.httpGet.port}} scheme: '{{data.blue.livenessProbe.httpGet.scheme}}' } - {%- endif %} + {%- endif -%} {% if data.blue.livenessProbe.tcpSocket -%} tcpSocket: { port: {{data.blue.livenessProbe.tcpSocket.port}} @@ -93,7 +101,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { port: {{data.blue.readinessProbe.httpGet.port}} scheme: '{{data.blue.readinessProbe.httpGet.scheme}}' } - {%- endif %} + {%- endif -%} {% if data.blue.readinessProbe.tcpSocket -%} tcpSocket: { port: {{data.blue.readinessProbe.tcpSocket.port}} @@ -118,7 +126,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { } ] serviceBinds: [ - {% for bind in data.serviceBinds %} + {% for bind in data.serviceBinds -%} { name: '{{ bind.name }}' serviceId: {{ bind.serviceId }} diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 4dcc4e1130a..e0d8902a9ed 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -15,7 +15,7 @@ module containerAppEnv 'environment.bicep' = { {% for item in data.certs %} module {{ item.moduleName }} '{{ item.templateName }}' = { - name: 'Cert{{ item.certName }}Deployment' + name: 'Cert{{ item.certName }}-Deployment' dependsOn: [ containerAppEnv ] @@ -27,7 +27,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {% for item in data.apps %} module {{ item.moduleName }} '{{ item.templateName }}' = { - name: '{{ item.appName }}Deployment' + name: '{{ item.appName }}-Deployment' dependsOn: [ containerAppEnv ] From c90c4da38d7dba86667003977ab1d14a1572464f Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 20 Feb 2025 02:20:12 +0800 Subject: [PATCH 23/98] scale --- .../migration/converter/app_converter.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index e3bd3390b5b..28028d5af3d 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -27,7 +27,7 @@ def calculate_data(self): "ingress": ingress, "isPublic": isPublic, "minReplicas": 1, - "maxReplicas": 5, + "maxReplicas": blueDeployment.get("capacity", 5), # "serviceBinds": serviceBinds, "blue": blueDeployment, "green": greenDeployment, @@ -78,6 +78,8 @@ def _get_deployments(self, source): 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') deployment = { "name": deployment_name, "env": [ @@ -90,6 +92,8 @@ def _get_deployments(self, source): "readinessProbe": self._convert_probe(readiness_probe, tier), "cpuCore": cpuCore, "memorySize": self._get_memory_by_cpu(cpuCore) or memorySize, + "scale": self._convert_scale(scale), + "capacity": capacity, } deployments.append(deployment) @@ -179,3 +183,10 @@ def _get_ingress(self, source, tier): "transport": ingress.get('backendProtocol').replace("Default", "auto"), "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", []) + } From b14025607d5bdcfebb4e77144f28154dbead0217 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 20 Feb 2025 18:10:50 +0800 Subject: [PATCH 24/98] vnet support --- .../migration/converter/app_converter.py | 22 ++++++-- .../migration/converter/conversion_context.py | 51 ++++++++++++------- .../converter/environment_converter.py | 9 +++- .../migration/converter/eureka_converter.py | 4 +- .../migration/converter/main_converter.py | 1 + .../migration/converter/param_converter.py | 4 +- .../converter/templates/environment.bicep.j2 | 30 ++++++----- .../converter/templates/main.bicep.j2 | 10 ++-- .../converter/templates/param.bicepparam.j2 | 9 ++-- 9 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 485ceab6b8a..084446fd0f1 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -6,6 +6,8 @@ class AppConverter(ConverterTemplate): def load_source(self, source): self.source = source + self.managed_components = source['managedComponents'] + self.is_enterprise = source['isEnterprise'] # print(f"App source: {self.source}") def calculate_data(self): @@ -37,7 +39,7 @@ def get_app_name(input_string): return input_string.split('/')[-1] def _get_service_bind(self, source, envName): - enable_sba = source['enabled_sba'] + enable_sba = self.managed_components['sba'] service_bind = [] addon = source['properties'].get('addonConfigs') @@ -50,11 +52,23 @@ def _get_service_bind(self, source, envName): "name": "bind-config", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" }) + if self.is_enterprise != True and self.managed_components['config'] == True: + # 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 addon.get('serviceRegistry') is not None and addon['serviceRegistry'].get('resourceId') is not None: service_bind.append({ "name": "bind-eureka", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" }) + if self.is_enterprise != True and self.managed_components['eureka'] == True: + # 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 enable_sba: service_bind.append({ "name": "bind-sba", @@ -117,7 +131,7 @@ def _get_memory_by_cpu(self, cpu): # create a method _convert_probe to convert the probe from the source to the target format def _convert_probe(self, probe, tier): - print(f"probe: {probe}") + # print(f"probe: {probe}") if probe is None: return None if probe.get("disableProbe") == True: @@ -157,7 +171,7 @@ def _convert_tcp_probe_action(self, probe, tier): } else: probeAction = None - print(f"probeAction: {probeAction}") + # print(f"probeAction: {probeAction}") return probeAction def _convert_http_probe_action(self, probe, tier): @@ -170,6 +184,6 @@ def _convert_http_probe_action(self, probe, tier): } else: probeAction = None - print(f"probeAction: {probeAction}") + # print(f"probeAction: {probeAction}") return probeAction diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index c4813373876..65eccd94260 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -42,18 +42,24 @@ def run_converters(self, source): source_wrapper = SourceDataWrapper(source) asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] - converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert( - asa_service - ) + # Environment Converter + is_vnet = self._is_vnet(asa_service) + is_enterprise = self._is_enterprise_tier(asa_service) + asa_service['isVnet'] = is_vnet + converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert(asa_service) + + # Cert Converter asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') asa_kv_certs = [] - for cert in asa_certs: certName = cert['name'].split('/')[-1] if cert['properties'].get('type') == "KeyVaultCertificate": asa_kv_certs.append(cert) converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) + elif cert['properties'].get('type') == "ContentCertificate": + converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) + # Managed components Converter managed_components = { 'gateway': False, 'config': False, @@ -65,26 +71,31 @@ def run_converters(self, source): converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) + # Apps Converter asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') asa_deployments = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') - # print(asa_deployments) - for app in asa_apps: - appName = app['name'].split('/')[-1] - app['enabled_sba'] = managed_components['sba'] - app['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app['name']}/")] - converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) + for app_source in asa_apps: + appName = app_source['name'].split('/')[-1] + app_source['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app_source['name']}/")] + app_source['managedComponents'] = managed_components + app_source['isEnterprise'] = is_enterprise + converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app_source) - main_source = { + # Param, readme and main Converter + full_source = { + "asa": asa_service, "apps": asa_apps, "certs": asa_kv_certs, "managedComponents": managed_components, + "isVnet": is_vnet, + "inEnterprise": is_enterprise, } - converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(asa_apps) - converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) - converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert( - main_source - ) + + converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(full_source) + converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(full_source) + converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert(full_source) + return converted_contents def save_to_files(self, converted_contents, output_path): @@ -151,12 +162,18 @@ def _convert_eureka_and_service_registry(self, source_wrapper, converted_content if not is_enterprise_tier: managed_components['eureka'] = True eureka_key = self.get_converter(EurekaConverter).get_template_name() - converted_contents[eureka_key] = self.get_converter(EurekaConverter).convert() + converted_contents[eureka_key] = self.get_converter(EurekaConverter).convert(None) return converted_contents def _is_enterprise_tier(self, asa_service): return asa_service['sku']['tier'] == 'Enterprise' + def _is_vnet(self, asa_service): + networkProfile = asa_service['properties'].get('networkProfile') + if networkProfile is None: + return False + return networkProfile.get('appSubnetId') is not None + class SourceDataWrapper: def __init__(self, source): self.source = source diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 5f922a006e0..05b74040686 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -12,6 +12,12 @@ def calculate_data(self): "containerAppLogAnalyticsName": f"log-{name}", } + isVnet = self.source['isVnet'] + if isVnet: + self.data["vnetConfiguration"] = { + "internal": str(True).lower(), + } + asa_zone_redundant = self.source['properties'].get('zoneRedundant') if asa_zone_redundant is not None: self.data["zoneRedundant"] = str(asa_zone_redundant).lower() @@ -25,7 +31,6 @@ def calculate_data(self): }] self.data["scheduledEntries"] = aca_maintenance_window - asa_certs = self.source['properties'].get('certificates') def get_template_name(self): - return "environment.bicep" \ No newline at end of file + return "environment.bicep" diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 6798fb3d3ec..08ce3b6b218 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -5,8 +5,8 @@ class EurekaConverter(ConverterTemplate): def __init__(self): super().__init__() - def load_source(self): - pass + def load_source(self, source): + self.source = source def calculate_data(self): name = "eureka" diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index b227836caf4..e52958828b4 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -9,6 +9,7 @@ def load_source(self, source): self.certs = source["certs"] def calculate_data(self): + self.data["isVnet"] = self.source.get("isVnet", False) self.data.setdefault("certs", []) for item in self.certs: certName = item['name'].split('/')[-1] diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 01e2e16491c..79c02fef28a 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -3,7 +3,8 @@ # Concrete Converter Subclass for paramter class ParamConverter(ConverterTemplate): def load_source(self, source): - self.apps = source + self.apps = source['apps'] + self.is_vnet = source['isVnet'] def calculate_data(self): self.data.setdefault("apps", []) @@ -13,6 +14,7 @@ def calculate_data(self): "appName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), }) + self.data["isVnet"] = self.is_vnet def get_template_name(self): return "param.bicepparam" \ 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 index e02bc23f73b..ecf5410f1be 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -1,13 +1,13 @@ // Params -param workloadProfileName string -param workloadProfileType string +param workloadProfileName = 'Dedicated' +param workloadProfileType = 'D4' +param minNodes = 1 +param maxNodes = 10 -param managedEnvName string = '{{data.containerAppEnvName}}' -param containerAppLogAnalyticsName string = '{{data.containerAppLogAnalyticsName}}' -param zoneRedundant bool = {{data.zoneRedundant}} +param vnetSubnetId string = '' resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { - name: containerAppLogAnalyticsName + name: '{{data.containerAppLogAnalyticsName}}' location: resourceGroup().location properties: { sku: { @@ -31,24 +31,30 @@ resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr } resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { - name: managedEnvName + name: '{{data.containerAppEnvName}}' location: resourceGroup().location + {% if data.identity %} identity: { type: 'SystemAssigned' } + {% endif %} properties: { - // vnetConfiguration: {} + {% if data.vnetConfiguration %} + vnetConfiguration: { + internal: {{ data.vnetConfiguration.internal }} + infrastructureSubnetId: vnetSubnetId + } + {% endif %} appLogsConfiguration: { destination: 'azure-monitor' } - zoneRedundant: zoneRedundant - // customDomainConfiguration: {} + zoneRedundant: {{data.zoneRedundant}} workloadProfiles: [ { name: workloadProfileName workloadProfileType: workloadProfileType - // minimumCount: minNodes - // maximumCount: maxNodes + minimumCount: minNodes + maximumCount: maxNodes } ] } diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 4dcc4e1130a..cd5f5fd4488 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -1,6 +1,7 @@ // Params -param workloadProfileName string -param workloadProfileType string +{% if data.isVnet == true %} +param vnetSubnetId string +{% endif %} {% for app in data.apps %} param {{app.containerAppImageName}} string {% endfor %} @@ -8,8 +9,9 @@ param {{app.containerAppImageName}} string module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' params: { - workloadProfileName: workloadProfileName - workloadProfileType: workloadProfileType + {% if data.isVnet == true %} + vnetSubnetId: vnetSubnetId + {% 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 index 61dcda140b4..b48d1462e4c 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -1,8 +1,9 @@ using './main.bicep' -param workloadProfileName = 'Consumption' -param workloadProfileType = 'Consumption' - {% for app in data.apps %} param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' -{% endfor %} \ No newline at end of file +{% endfor %} +{% if data.isVnet == true %} +// Replace this wth the actual vnet subnet id +param vnetSubnetId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet' +{% endif %} \ No newline at end of file From adbb7ad1dde5e31a0549297c8ef7c1519d7c11e2 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 21 Feb 2025 15:21:59 +0800 Subject: [PATCH 25/98] identity --- .../migration/converter/app_converter.py | 5 ++++- .../migration/converter/templates/app.bicep.j2 | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 28028d5af3d..d9a5d5f9851 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -19,6 +19,8 @@ def calculate_data(self): tier = blueDeployment.get('sku', {}).get('tier') ingress = self._get_ingress(self.source, tier) isPublic = self.source['properties'].get('public') + identity = self.source.get('identity') + print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") self.data = { "containerAppName": appName, @@ -31,7 +33,8 @@ def calculate_data(self): # "serviceBinds": serviceBinds, "blue": blueDeployment, "green": greenDeployment, - "isBlueGreen": len(deployments) > 1 + "isBlueGreen": len(deployments) > 1, + "identity": identity, } def get_template_name(self): diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index d808c54ba94..cbeef57f9da 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -14,6 +14,19 @@ param containerAppEnvId 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 configuration: { From e0655db673cae6978fc776e00de1293765ce9b23 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 21 Feb 2025 15:29:30 +0800 Subject: [PATCH 26/98] rollback --- .../migration/converter/app_converter.py | 4 ++-- .../migration/converter/conversion_context.py | 24 +++++++++---------- .../migration/converter/main_converter.py | 6 ++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index d9a5d5f9851..2cf3373e222 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -12,7 +12,7 @@ def calculate_data(self): appName = self.source['name'].split('/')[-1] envName = self.source['name'].split('/')[0] moduleName = appName.replace("-", "_") - # serviceBinds = self._get_service_bind(self.source, envName) + serviceBinds = self._get_service_bind(self.source, envName) deployments = self._get_deployments(self.source) blueDeployment = deployments[0] if len(deployments) > 0 else {} greenDeployment = deployments[1] if len(deployments) > 1 else {} @@ -30,7 +30,7 @@ def calculate_data(self): "isPublic": isPublic, "minReplicas": 1, "maxReplicas": blueDeployment.get("capacity", 5), - # "serviceBinds": serviceBinds, + "serviceBinds": serviceBinds, "blue": blueDeployment, "green": greenDeployment, "isBlueGreen": len(deployments) > 1, diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index f94327376ac..c4813373876 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -54,16 +54,16 @@ def run_converters(self, source): asa_kv_certs.append(cert) converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) - # managed_components = { - # 'gateway': False, - # 'config': False, - # 'eureka': False, - # 'sba': False, - # } - # converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) - # converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, managed_components) - # converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) - # converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) + managed_components = { + 'gateway': False, + 'config': False, + 'eureka': False, + 'sba': False, + } + converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) + converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, managed_components) + converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) + converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') asa_deployments = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') @@ -71,14 +71,14 @@ def run_converters(self, source): for app in asa_apps: appName = app['name'].split('/')[-1] - # app['enabled_sba'] = managed_components['sba'] + app['enabled_sba'] = managed_components['sba'] app['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app['name']}/")] converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app) main_source = { "apps": asa_apps, "certs": asa_kv_certs, - # "managedComponents": managed_components, + "managedComponents": managed_components, } converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(asa_apps) converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(None) diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 49b7628cb9b..b227836caf4 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -5,7 +5,7 @@ class MainConverter(ConverterTemplate): def load_source(self, source): self.source = source self.apps = source["apps"] - # self.managedComponents = source["managedComponents"] + self.managedComponents = source["managedComponents"] self.certs = source["certs"] def calculate_data(self): @@ -34,8 +34,8 @@ def calculate_data(self): } self.data["apps"].append(appData) - # for name, value in self.managedComponents.items(): - # self.data[name] = value + for name, value in self.managedComponents.items(): + self.data[name] = value def get_template_name(self): return "main.bicep" \ No newline at end of file From b8a2063bd6e8a131129132612f0035eefef19813 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 21 Feb 2025 15:55:14 +0800 Subject: [PATCH 27/98] fix issue --- .../migration/converter/templates/environment.bicep.j2 | 8 ++++---- .../migration/converter/templates/main.bicep.j2 | 9 +++++++++ .../migration/converter/templates/param.bicepparam.j2 | 5 +++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index ecf5410f1be..19d24156d28 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -1,8 +1,8 @@ // Params -param workloadProfileName = 'Dedicated' -param workloadProfileType = 'D4' -param minNodes = 1 -param maxNodes = 10 +param workloadProfileName string +param workloadProfileType string +param minNodes int +param maxNodes int param vnetSubnetId string = '' diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index e2c3d9de4c5..a5d80b6f60e 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -1,4 +1,9 @@ // Params +param workloadProfileName string +param workloadProfileType string +param minNodes int +param maxNodes int + {% if data.isVnet == true %} param vnetSubnetId string {% endif %} @@ -9,6 +14,10 @@ param {{app.containerAppImageName}} string module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' params: { + workloadProfileName: workloadProfileName + workloadProfileType: workloadProfileType + minNodes: minNodes + maxNodes: maxNodes {% if data.isVnet == true %} vnetSubnetId: vnetSubnetId {% 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 index b48d1462e4c..0cb775ac861 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -1,5 +1,10 @@ using './main.bicep' +param workloadProfileName = 'Dedicated' +param workloadProfileType = 'D4' +param minNodes = 1 +param maxNodes = 10 + {% for app in data.apps %} param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' {% endfor %} From 377ceef13e37d2c92b9be2e73639b6b0ed3f1511 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Sat, 22 Feb 2025 15:52:29 +0800 Subject: [PATCH 28/98] add targetPort to param --- .../azext_spring/migration/converter/app_converter.py | 1 + .../migration/converter/main_converter.py | 1 + .../migration/converter/param_converter.py | 1 + .../migration/converter/templates/app.bicep.j2 | 11 ++++++----- .../converter/templates/environment.bicep.j2 | 5 +++-- .../migration/converter/templates/main.bicep.j2 | 6 ++++-- .../migration/converter/templates/param.bicepparam.j2 | 3 ++- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index f2c425b7c9a..5f7d06eb964 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -27,6 +27,7 @@ def calculate_data(self): self.data = { "containerAppName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "targetPort": "targetPortFor_"+appName.replace("-", "_"), "moduleName": moduleName, "ingress": ingress, "isPublic": isPublic, diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index e52958828b4..c6d2b6641eb 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -32,6 +32,7 @@ def calculate_data(self): "moduleName": moduleName, "templateName": templateName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "targetPort": "targetPortFor_"+appName.replace("-", "_"), } self.data["apps"].append(appData) diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 79c02fef28a..09c1bc82139 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -13,6 +13,7 @@ def calculate_data(self): self.data["apps"].append({ "appName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "targetPort": "targetPortFor_"+appName.replace("-", "_"), }) self.data["isVnet"] = self.is_vnet diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index cbeef57f9da..8663a1e3efd 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -1,5 +1,6 @@ param containerAppName string = '{{data.containerAppName}}' param {{data.containerAppImageName}} string +param {{data.targetPort}} int @description('Minimum number of replicas that will be deployed') @minValue(0) @@ -33,7 +34,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { ingress: { {% if data.ingress -%} external: {% if data.isPublic %}true{% else %}false{% endif %} - targetPort: {{data.ingress.targetPort}} + targetPort: {{data.targetPort}} allowInsecure: false transport: '{{data.ingress.transport}}' {% if data.ingress.sessionAffinity -%} @@ -79,13 +80,13 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { {% if data.blue.livenessProbe.httpGet -%} httpGet: { path: '{{data.blue.livenessProbe.httpGet.path}}' - port: {{data.blue.livenessProbe.httpGet.port}} + port: {{data.targetPort}} scheme: '{{data.blue.livenessProbe.httpGet.scheme}}' } {%- endif -%} {% if data.blue.livenessProbe.tcpSocket -%} tcpSocket: { - port: {{data.blue.livenessProbe.tcpSocket.port}} + port: {{data.targetPort}} } {%- endif %} {% if data.blue.livenessProbe.initialDelaySeconds -%} @@ -111,13 +112,13 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { {% if data.blue.readinessProbe.httpGet -%} httpGet: { path: '{{data.blue.readinessProbe.httpGet.path}}' - port: {{data.blue.readinessProbe.httpGet.port}} + port: {{data.targetPort}} scheme: '{{data.blue.readinessProbe.httpGet.scheme}}' } {%- endif -%} {% if data.blue.readinessProbe.tcpSocket -%} tcpSocket: { - port: {{data.blue.readinessProbe.tcpSocket.port}} + port: {{data.targetPort}} } {%- endif %} {% if data.blue.readinessProbe.initialDelaySeconds -%} diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 19d24156d28..1563bb6df90 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -3,8 +3,9 @@ param workloadProfileName string param workloadProfileType string param minNodes int param maxNodes int - -param vnetSubnetId string = '' +{% if data.vnetConfiguration -%} +param vnetSubnetId string +{%- endif %} resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: '{{data.containerAppLogAnalyticsName}}' diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index a5d80b6f60e..8189a6ac0b5 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -9,6 +9,7 @@ param vnetSubnetId string {% endif %} {% for app in data.apps %} param {{app.containerAppImageName}} string +param {{app.targetPort}} int {% endfor %} module containerAppEnv 'environment.bicep' = { @@ -18,9 +19,9 @@ module containerAppEnv 'environment.bicep' = { workloadProfileType: workloadProfileType minNodes: minNodes maxNodes: maxNodes - {% if data.isVnet == true %} + {% if data.isVnet == true -%} vnetSubnetId: vnetSubnetId - {% endif %} + {%- endif %} } } @@ -45,6 +46,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId {{item.containerAppImageName}}: {{item.containerAppImageName}} + {{item.targetPort}}: {{item.targetPort}} } } {% endfor %} diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 0cb775ac861..d115d9a397f 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -7,8 +7,9 @@ param maxNodes = 10 {% for app in data.apps %} param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' +param {{app.targetPort}} = 80 {% endfor %} {% if data.isVnet == true %} // Replace this wth the actual vnet subnet id -param vnetSubnetId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet' +param vnetSubnetId = '' {% endif %} \ No newline at end of file From a90e6b3f856db6694bd6ae0d99f20a36d4de3d93 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Sat, 22 Feb 2025 20:50:56 +0800 Subject: [PATCH 29/98] add identity to aca environment --- .../migration/converter/app_converter.py | 2 +- .../migration/converter/conversion_context.py | 5 ++- .../converter/environment_converter.py | 33 +++++++++++++++++++ .../converter/templates/environment.bicep.j2 | 14 ++++++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 5f7d06eb964..d0793abbed7 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -22,7 +22,7 @@ def calculate_data(self): ingress = self._get_ingress(self.source, tier) isPublic = self.source['properties'].get('public') identity = self.source.get('identity') - print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") + # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") self.data = { "containerAppName": appName, diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 65eccd94260..58fa3f9495e 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -41,11 +41,13 @@ def run_converters(self, source): converted_contents = {} source_wrapper = SourceDataWrapper(source) asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') # Environment Converter is_vnet = self._is_vnet(asa_service) is_enterprise = self._is_enterprise_tier(asa_service) asa_service['isVnet'] = is_vnet + asa_service['apps'] = asa_apps converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert(asa_service) # Cert Converter @@ -71,10 +73,7 @@ def run_converters(self, source): converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) - # Apps Converter - asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') asa_deployments = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') - for app_source in asa_apps: appName = app_source['name'].split('/')[-1] app_source['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app_source['name']}/")] diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 05b74040686..299839eee5b 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -7,9 +7,14 @@ def load_source(self, source): def calculate_data(self): name = self.source['name'].split('/')[-1] + apps = self.source.get('apps') self.data = { "containerAppEnvName": name, "containerAppLogAnalyticsName": f"log-{name}", + "identity": { + "type": self._get_identity_type(apps), + "userAssignedIdentities": self._get_user_assigned_identity_list(apps), + } } isVnet = self.source['isVnet'] @@ -34,3 +39,31 @@ def calculate_data(self): def get_template_name(self): return "environment.bicep" + + def _get_identity_type(self, apps): + type = None + hasUserAssignedIdentity = False + hasSystemAssignedIdentity = False + for app in apps: + if app.get('identity') is not None: + if 'SystemAssigned' in app['identity'].get('type'): + hasSystemAssignedIdentity = True + elif 'UserAssigned' in app['identity'].get('type'): + hasUserAssignedIdentity = True + if hasUserAssignedIdentity and hasSystemAssignedIdentity: + type = "SystemAssigned,UserAssigned" + elif hasUserAssignedIdentity: + type = "UserAssigned" + elif hasSystemAssignedIdentity: + type = "SystemAssigned" + return type + + def _get_user_assigned_identity_list(self, apps): + user_assigned_identities = [] + for app in apps: + if app.get('identity') is not None: + if 'UserAssigned' in app['identity'].get('type'): + if app.get('identity').get('userAssignedIdentities') is not None: + for id in app['identity']['userAssignedIdentities']: + user_assigned_identities.append(id) + return user_assigned_identities diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 1563bb6df90..6bf7164bddd 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -34,11 +34,19 @@ resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { name: '{{data.containerAppEnvName}}' location: resourceGroup().location - {% if data.identity %} + {% if data.identity.type -%} identity: { - type: 'SystemAssigned' + type: '{{data.identity.type}}' + {% if data.identity.userAssignedIdentities -%} + userAssignedIdentities: { + {% for miResourceId in data.identity.userAssignedIdentities -%} + '{{miResourceId}}': {} + {% if not loop.last %}{% endif %} + {%- endfor %} + } + {%- endif %} } - {% endif %} + {%- endif %} properties: { {% if data.vnetConfiguration %} vnetConfiguration: { From 710c874365652f3c8614b86ad25626ea31c4a53c Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 24 Feb 2025 15:52:36 +0800 Subject: [PATCH 30/98] add app workloadprofiletype --- .../azext_spring/migration/converter/templates/app.bicep.j2 | 2 ++ .../azext_spring/migration/converter/templates/main.bicep.j2 | 1 + .../migration/converter/templates/param.bicepparam.j2 | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index 8663a1e3efd..cdf248c83cc 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -11,6 +11,7 @@ param minReplicas int = {{data.minReplicas}} param maxReplicas int = {{data.maxReplicas}} param containerAppEnvId string +param workloadProfileName string resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { name: containerAppName @@ -30,6 +31,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { {%- endif %} properties: { managedEnvironmentId: containerAppEnvId + workloadProfileName: workloadProfileName configuration: { ingress: { {% if data.ingress -%} diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 8189a6ac0b5..1dde690526a 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -45,6 +45,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { ] params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId + workloadProfileName: workloadProfileName {{item.containerAppImageName}}: {{item.containerAppImageName}} {{item.targetPort}}: {{item.targetPort}} } diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index d115d9a397f..2e948764549 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -10,6 +10,7 @@ param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps param {{app.targetPort}} = 80 {% endfor %} {% if data.isVnet == true %} -// Replace this wth the actual vnet subnet id -param vnetSubnetId = '' +// Provide the full resource ID of the existing subnet. +// Example: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} +param vnetSubnetId {% endif %} \ No newline at end of file From 449e60b30a7f391b302cb1961558766cc19021c1 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 24 Feb 2025 17:29:39 +0800 Subject: [PATCH 31/98] Config server/ACS secrets parameterize --- .../migration/converter/acs_converter.py | 51 +++++++++++-------- .../converter/config_server_converter.py | 51 +++++++++++-------- .../templates/config_server.bicep.j2 | 10 +++- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 96aab2048ec..22f32b1b8b4 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -22,11 +22,12 @@ def load_source(self, source): def calculate_data(self): name = f"config" - configurations = self._get_configurations(self.source) + configurations, params = self._get_configurations_and_params(self.source) replicas = 2 self.data = { "configServerName": name, + "params": params, "configurations": configurations, "replicas": replicas } @@ -34,36 +35,37 @@ def calculate_data(self): def get_template_name(self): return "config_server.bicep" - def _get_configurations(self, source): - configurations = [] + def _get_configurations_and_params(self, source): + configurations = [] + params = [] git_repos = source['properties']['settings']['gitProperty']['repositories'] if len(git_repos) > 0: default_repo = git_repos[0] - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, default_repo.get('uri')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, default_repo.get('label')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, default_repo.get('searchPaths')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, default_repo.get('username')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, default_repo.get('password')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, default_repo.get('privateKey')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, default_repo.get('hostKey')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, default_repo.get('hostKeyAlgorithm')) + 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) 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_existss(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm')) + 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) - return configurations + return configurations, params - def _add_property_if_existss(self, configurations, key, value): + def _add_property_if_exists(self, configurations, key, value): if value: if isinstance(value, (list, tuple)): value = ",".join(map(str, value)) @@ -71,4 +73,9 @@ def _add_property_if_existss(self, configurations, key, value): "propertyName": key, "value": value }) - \ No newline at end of file + + 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) \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index d5200848336..3f612da2efb 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -22,11 +22,12 @@ def load_source(self, source): def calculate_data(self): name = f"config" - configurations = self._get_configurations(self.source) + configurations, params = self._get_configurations_and_params(self.source) replicas = 2 self.data = { "configServerName": name, + "params": params, "configurations": configurations, "replicas": replicas } @@ -34,36 +35,37 @@ def calculate_data(self): def get_template_name(self): return "config_server.bicep" - def _get_configurations(self, source): + def _get_configurations_and_params(self, source): configurations = [] + params = [] git_property = source.get('properties', {}).get('configServer', {}).get('gitProperty') if git_property is not None: - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, git_property.get('uri')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, git_property.get('label')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, git_property.get('searchPaths')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, git_property.get('username')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, git_property.get('password')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, git_property.get('privateKey')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, git_property.get('hostKey')) - self._add_property_if_existss(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, git_property.get('hostKeyAlgorithm')) + 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_existss(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm')) - self._add_property_if_existss(configurations, configuration_key_repo_prefix + self.KEY_PATTERN, repo.get('pattern')) + 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 + return configurations, params - def _add_property_if_existss(self, configurations, key, value): + def _add_property_if_exists(self, configurations, key, value): if value: if isinstance(value, (list, tuple)): value = ",".join(map(str, value)) @@ -71,4 +73,9 @@ def _add_property_if_existss(self, configurations, key, value): "propertyName": key, "value": value }) - \ No newline at end of file + + 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) \ No newline at end of file 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 index 7d82eefed8a..827300175af 100644 --- a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -1,5 +1,13 @@ 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 = 'username' +{%- for param in data.params %} +param {{param}} +{%- endfor %} +{%- endif %} + resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-preview' = { name: '${managedEnvironments_aca_env_name}/{{ data.configServerName }}' properties: { @@ -8,7 +16,7 @@ resource configServerTest 'Microsoft.App/managedEnvironments/javaComponents@2024 {% for config in data.configurations %} { propertyName: '{{ config.propertyName }}' - value: '{{ config.value }}' + value: {% if config.value in data.params %}{{ config.value }}{% else %}'{{ config.value }}'{% endif %} }{% if not loop.last %}{% endif %} {% endfor %} ] From fe64a087cbf100fedd86f2ffa555a72441ee966f Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Mon, 24 Feb 2025 21:05:16 +0800 Subject: [PATCH 32/98] add startupprobe and refactor env --- .../migration/converter/app_converter.py | 62 ++++++++++++++----- .../converter/templates/app.bicep.j2 | 32 ++++++++++ .../converter/templates/readme.md.j2 | 2 +- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index d0793abbed7..08ff696098a 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -1,7 +1,10 @@ import re +from knack.log import get_logger from .base_converter import ConverterTemplate +logger = get_logger(__name__) + # Concrete Converter Subclass for Container App class AppConverter(ConverterTemplate): def load_source(self, source): @@ -92,6 +95,7 @@ def _get_deployments(self, source): 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") @@ -100,14 +104,10 @@ def _get_deployments(self, source): capacity = deployment.get('sku', {}).get('capacity') deployment = { "name": deployment_name, - "env": [ - { - "name": key, - "value": value - } for key, value in env.items() - ], + "env": self._convert_env(env), "livenessProbe": self._convert_probe(liveness_probe, tier), "readinessProbe": self._convert_probe(readiness_probe, tier), + "startupProbe": self._convert_probe(startup_probe, tier), "cpuCore": cpuCore, "memorySize": self._get_memory_by_cpu(cpuCore) or memorySize, "scale": self._convert_scale(scale), @@ -118,6 +118,16 @@ def _get_deployments(self, source): # print(f"deployments: {deployments}") return deployments + 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): @@ -147,26 +157,46 @@ def _convert_probe(self, probe, tier): if probe is None: return None if probe.get("disableProbe") == True: - print(f"Probe is disabled") + logger.debug(f"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']. initialDelaySeconds = 60 - result = { - "initialDelaySeconds": initialDelaySeconds, - "periodSeconds": probe.get("periodSeconds", None), - "timeoutSeconds": probe.get("timeoutSeconds", None), - "successThreshold": probe.get("successThreshold", None), - "failureThreshold": probe.get("failureThreshold", None), - } + 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 - return result + execAction = self._convert_exec_probe_action(probe, tier) + if execAction is not None: + logger.warning(f"Mismatch: The ExecAction {execAction} 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 = {} @@ -176,7 +206,6 @@ def _convert_tcp_probe_action(self, probe, tier): } else: probeAction = None - # print(f"probeAction: {probeAction}") return probeAction def _convert_http_probe_action(self, probe, tier): @@ -189,7 +218,6 @@ def _convert_http_probe_action(self, probe, tier): } else: probeAction = None - # print(f"probeAction: {probeAction}") return probeAction def _get_ingress(self, source, tier): diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index 8663a1e3efd..2f2a67f8069 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -136,6 +136,38 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { 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.targetPort}} + scheme: '{{data.blue.startupProbe.httpGet.scheme}}' + } + {%- endif -%} + {% if data.blue.startupProbe.tcpSocket -%} + tcpSocket: { + port: {{data.targetPort}} + } + {%- 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 %} ] } ] diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index ed26dd42d87..c5050495d93 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -44,7 +44,7 @@ Modify values in the Bicep files as needed to align with your environment. az group create --name --location ``` -#### Deploy the Bicep files using the Azure CLI: +### Deploy the Bicep files using the Azure CLI: ```azurecli az deployment group create --resource-group --template-file main.bicep --parameters param.bicepparam From 3d8b2f0c4cc0b6af38bfc7bdf0f59f6a3db20eef Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Mon, 24 Feb 2025 23:14:37 +0800 Subject: [PATCH 33/98] remove blank line from main.bicep --- .../converter/templates/main.bicep.j2 | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 8189a6ac0b5..1fc6b49d253 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -4,13 +4,13 @@ param workloadProfileType string param minNodes int param maxNodes int -{% if data.isVnet == true %} +{%- if data.isVnet == true %} param vnetSubnetId string -{% endif %} -{% for app in data.apps %} +{%- endif %} +{%- for app in data.apps %} param {{app.containerAppImageName}} string param {{app.targetPort}} int -{% endfor %} +{%- endfor %} module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' @@ -25,7 +25,7 @@ module containerAppEnv 'environment.bicep' = { } } -{% for item in data.certs %} +{%- for item in data.certs %} module {{ item.moduleName }} '{{ item.templateName }}' = { name: 'Cert{{ item.certName }}-Deployment' dependsOn: [ @@ -35,9 +35,9 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } -{% endfor %} +{%- endfor %} -{% for item in data.apps %} +{%- for item in data.apps %} module {{ item.moduleName }} '{{ item.templateName }}' = { name: '{{ item.appName }}-Deployment' dependsOn: [ @@ -49,9 +49,9 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {{item.targetPort}}: {{item.targetPort}} } } -{% endfor %} +{%- endfor %} -{% if data.gateway == true %} +{%- if data.gateway == true %} module managedGateway 'gateway.bicep' = { name: 'gatewayDeployment' dependsOn: [ @@ -61,9 +61,9 @@ module managedGateway 'gateway.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } -{% endif %} +{%- endif %} -{% if data.config == true %} +{%- if data.config == true %} module managedConfig 'config_server.bicep' = { name: 'configServerDeployment' dependsOn: [ @@ -73,9 +73,9 @@ module managedConfig 'config_server.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } -{% endif %} +{%- endif %} -{% if data.eureka == true %} +{%- if data.eureka == true %} module managedEureka 'eureka.bicep' = { name: 'eurekaDeployment' dependsOn: [ @@ -85,9 +85,9 @@ module managedEureka 'eureka.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } -{% endif %} +{%- endif %} -{% if data.sba == true %} +{%- if data.sba == true %} module managedSpringBootAdmin 'spring_boot_admin.bicep' = { name: 'springBootAdminDeployment' dependsOn: [ @@ -97,4 +97,4 @@ module managedSpringBootAdmin 'spring_boot_admin.bicep' = { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } -{% endif %} +{%- endif %} From 714cc434582df96b99ad3ad326982e77b37f46ec Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Tue, 25 Feb 2025 16:15:28 +0800 Subject: [PATCH 34/98] Remove empty line --- .../migration/converter/acs_converter.py | 28 +++---- .../migration/converter/gateway_converter.py | 29 +++---- .../converter/templates/app.bicep.j2 | 78 +++++++++---------- .../converter/templates/cert.bicep.j2 | 8 +- .../templates/config_server.bicep.j2 | 10 +-- .../converter/templates/environment.bicep.j2 | 22 +++--- .../converter/templates/eureka.bicep.j2 | 6 +- .../converter/templates/gateway.bicep.j2 | 26 ++++--- .../converter/templates/main.bicep.j2 | 2 +- .../converter/templates/param.bicepparam.j2 | 13 ++-- .../templates/spring_boot_admin.bicep.j2 | 6 +- 11 files changed, 115 insertions(+), 113 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 22f32b1b8b4..144b5c2d7b0 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -5,7 +5,7 @@ class ACSConverter(ConverterTemplate): CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" KEY_URI = ".uri" - KEY_LABEL = ".label" + KEY_LABEL = ".default-label" KEY_SEARCH_PATHS = ".search-paths" KEY_USERNAME = ".username" KEY_PASSWORD = ".password" @@ -38,9 +38,9 @@ def get_template_name(self): def _get_configurations_and_params(self, source): configurations = [] params = [] - git_repos = source['properties']['settings']['gitProperty']['repositories'] - if len(git_repos) > 0: + git_repos = source.get('properties', {}).get('settings', {}).get('gitProperty', {}).get('repositories') + 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')) @@ -51,17 +51,17 @@ def _get_configurations_and_params(self, source): 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) - 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) + 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) return configurations, params diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index d344e91ccd0..7c62cc25001 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -17,7 +17,9 @@ def load_source(self, source): def calculate_data(self): gatewayName = f"gateway" configurations = self._get_configurations(self.source) - replicas = min(2, self.source['gateway']['sku']['capacity']) + replicas = 2 + if self.source.get('gateway', {}).get('sku', {}).get('capacity') is not None: + replicas = min(2, self.source['gateway']['sku']['capacity']) routes = self._get_routes(self.source['routes']) self.data = { @@ -45,18 +47,19 @@ def _get_configurations(self, source): def _get_routes(self, routes): aca_routes = [] - for route in routes: - aca_id = route['name'].split('/')[-1] - aca_uri = self._get_uri_from_route(route) - if route.get('properties', {}).get('routes') is not None: - for r in route['properties']['routes']: - aca_routes.append({ - "id": aca_id, - "uri": r.get('uri', aca_uri), - "predicates": r.get('predicates'), - "filters": r.get('filters'), - "order": r.get('order') or 0, - }) + if routes: + for route in routes: + aca_id = route['name'].split('/')[-1] + aca_uri = self._get_uri_from_route(route) + if route.get('properties', {}).get('routes') is not None: + for r in route['properties']['routes']: + aca_routes.append({ + "id": aca_id, + "uri": r.get('uri', aca_uri), + "predicates": r.get('predicates') if r.get('predicates') else [], + "filters": r.get('filters') if r.get('filters') else [], + "order": r.get('order') or 0, + }) return aca_routes def _get_uri_from_route(self, route): diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index e800cacc2a6..7ec47fdc18a 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -16,14 +16,14 @@ param workloadProfileName string resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { name: containerAppName location: resourceGroup().location - {% if data.identity -%} + {%- if data.identity %} identity: { type: '{{data.identity.type}}' - {% if data.identity.userAssignedIdentities -%} + {%- if data.identity.userAssignedIdentities %} userAssignedIdentities: { - {% for miResourceId in data.identity.userAssignedIdentities -%} + {%- for miResourceId in data.identity.userAssignedIdentities %} '{{miResourceId}}': {} - {% if not loop.last %}{% endif %} + {%- if not loop.last %}{%- endif %} {%- endfor %} } {%- endif %} @@ -34,12 +34,12 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { workloadProfileName: workloadProfileName configuration: { ingress: { - {% if data.ingress -%} + {%- if data.ingress %} external: {% if data.isPublic %}true{% else %}false{% endif %} targetPort: {{data.targetPort}} allowInsecure: false transport: '{{data.ingress.transport}}' - {% if data.ingress.sessionAffinity -%} + {%- if data.ingress.sessionAffinity %} stickySessions: { affinity: '{{data.ingress.sessionAffinity}}' } @@ -66,105 +66,105 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { memory: '{{data.blue.memorySize}}' } env: [ - {% for env in data.blue.env -%} + {%- for env in data.blue.env %} { name: '{{ env.name }}' value: '{{ env.value }}' - }{% if not loop.last %}{% endif %} - {% endfor %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} ] probes: [ - {% if data.blue.livenessProbe -%} + {%- if data.blue.livenessProbe %} { - {% if data.blue.livenessProbe.periodSeconds -%} + {%- if data.blue.livenessProbe.periodSeconds %} periodSeconds: {{data.blue.livenessProbe.periodSeconds}} {%- endif %} - {% if data.blue.livenessProbe.httpGet -%} + {%- if data.blue.livenessProbe.httpGet %} httpGet: { path: '{{data.blue.livenessProbe.httpGet.path}}' port: {{data.targetPort}} scheme: '{{data.blue.livenessProbe.httpGet.scheme}}' } - {%- endif -%} - {% if data.blue.livenessProbe.tcpSocket -%} + {%- endif %} + {%- if data.blue.livenessProbe.tcpSocket %} tcpSocket: { port: {{data.targetPort}} } {%- endif %} - {% if data.blue.livenessProbe.initialDelaySeconds -%} + {%- if data.blue.livenessProbe.initialDelaySeconds %} initialDelaySeconds: {{data.blue.livenessProbe.initialDelaySeconds}} {%- endif %} - {% if data.blue.livenessProbe.timeoutSeconds -%} + {%- if data.blue.livenessProbe.timeoutSeconds %} timeoutSeconds: {{data.blue.livenessProbe.timeoutSeconds}} {%- endif %} - {% if data.blue.livenessProbe.successThreshold -%} + {%- if data.blue.livenessProbe.successThreshold %} successThreshold: {{data.blue.livenessProbe.successThreshold}} {%- endif %} - {% if data.blue.livenessProbe.failureThreshold -%} + {%- if data.blue.livenessProbe.failureThreshold %} failureThreshold: {{data.blue.livenessProbe.failureThreshold}} {%- endif %} type: 'Liveness' } {%- endif %} - {% if data.blue.readinessProbe -%} + {%- if data.blue.readinessProbe %} { - {% if data.blue.readinessProbe.periodSeconds -%} + {%- if data.blue.readinessProbe.periodSeconds %} periodSeconds: {{data.blue.readinessProbe.periodSeconds}} {%- endif %} - {% if data.blue.readinessProbe.httpGet -%} + {%- if data.blue.readinessProbe.httpGet %} httpGet: { path: '{{data.blue.readinessProbe.httpGet.path}}' port: {{data.targetPort}} scheme: '{{data.blue.readinessProbe.httpGet.scheme}}' } - {%- endif -%} - {% if data.blue.readinessProbe.tcpSocket -%} + {%- endif %} + {%- if data.blue.readinessProbe.tcpSocket %} tcpSocket: { port: {{data.targetPort}} } {%- endif %} - {% if data.blue.readinessProbe.initialDelaySeconds -%} + {%- if data.blue.readinessProbe.initialDelaySeconds %} initialDelaySeconds: {{data.blue.readinessProbe.initialDelaySeconds}} {%- endif %} - {% if data.blue.readinessProbe.timeoutSeconds -%} + {%- if data.blue.readinessProbe.timeoutSeconds %} timeoutSeconds: {{data.blue.readinessProbe.timeoutSeconds}} {%- endif %} - {% if data.blue.readinessProbe.successThreshold -%} + {%- if data.blue.readinessProbe.successThreshold %} successThreshold: {{data.blue.readinessProbe.successThreshold}} {%- endif %} - {% if data.blue.readinessProbe.failureThreshold -%} + {%- if data.blue.readinessProbe.failureThreshold %} failureThreshold: {{data.blue.readinessProbe.failureThreshold}} {%- endif %} type: 'Readiness' } {%- endif %} - {% if data.blue.startupProbe -%} + {%- if data.blue.startupProbe %} { - {% if data.blue.startupProbe.periodSeconds -%} + {%- if data.blue.startupProbe.periodSeconds %} periodSeconds: {{data.blue.startupProbe.periodSeconds}} {%- endif %} - {% if data.blue.startupProbe.httpGet -%} + {%- if data.blue.startupProbe.httpGet %} httpGet: { path: '{{data.blue.startupProbe.httpGet.path}}' port: {{data.targetPort}} scheme: '{{data.blue.startupProbe.httpGet.scheme}}' } - {%- endif -%} - {% if data.blue.startupProbe.tcpSocket -%} + {%- endif %} + {%- if data.blue.startupProbe.tcpSocket %} tcpSocket: { port: {{data.targetPort}} } {%- endif %} - {% if data.blue.startupProbe.initialDelaySeconds -%} + {%- if data.blue.startupProbe.initialDelaySeconds %} initialDelaySeconds: {{data.blue.startupProbe.initialDelaySeconds}} {%- endif %} - {% if data.blue.startupProbe.timeoutSeconds -%} + {%- if data.blue.startupProbe.timeoutSeconds %} timeoutSeconds: {{data.blue.startupProbe.timeoutSeconds}} {%- endif %} - {% if data.blue.startupProbe.successThreshold -%} + {%- if data.blue.startupProbe.successThreshold %} successThreshold: {{data.blue.startupProbe.successThreshold}} {%- endif %} - {% if data.blue.startupProbe.failureThreshold -%} + {%- if data.blue.startupProbe.failureThreshold %} failureThreshold: {{data.blue.startupProbe.failureThreshold}} {%- endif %} type: 'Startup' @@ -174,12 +174,12 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { } ] serviceBinds: [ - {% for bind in data.serviceBinds -%} + {%- for bind in data.serviceBinds %} { name: '{{ bind.name }}' serviceId: {{ bind.serviceId }} - }{% if not loop.last %}{% endif %} - {% endfor %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} ] scale: { minReplicas: minReplicas diff --git a/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 index 82a6ee84e26..36f2255cfcf 100644 --- a/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/cert.bicep.j2 @@ -5,15 +5,15 @@ resource {{data.moduleName}} 'Microsoft.App/managedEnvironments/certificates@202 location: resourceGroup().location properties: { certificateType: '{{ data.certificateType }}' - {% if data.certificateKeyVaultProperties %} + {%- if data.certificateKeyVaultProperties %} certificateKeyVaultProperties: { keyVaultUrl: '{{ data.certificateKeyVaultProperties.keyVaultUrl }}' identity: '{{ data.certificateKeyVaultProperties.identity }}' } - {% endif %} - {% if data.value %} + {%- endif %} + {%- if data.value %} value: '{{ data.value }}' password: '' - {% endif %} + {%- 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 index 827300175af..e260c3f8cb1 100644 --- a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -1,24 +1,22 @@ param managedEnvironments_aca_env_name string - -{% if data.params | length > 0 -%} +{%- 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 = 'username' {%- for param in data.params %} param {{param}} {%- 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 %} + {%- 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 %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} ] scale: { minReplicas: {{ data.replicas }} diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 6bf7164bddd..666f10ae0b1 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -3,7 +3,7 @@ param workloadProfileName string param workloadProfileType string param minNodes int param maxNodes int -{% if data.vnetConfiguration -%} +{%- if data.vnetConfiguration %} param vnetSubnetId string {%- endif %} @@ -34,26 +34,26 @@ resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { name: '{{data.containerAppEnvName}}' location: resourceGroup().location - {% if data.identity.type -%} + {%- if data.identity.type %} identity: { type: '{{data.identity.type}}' - {% if data.identity.userAssignedIdentities -%} + {%- if data.identity.userAssignedIdentities %} userAssignedIdentities: { - {% for miResourceId in data.identity.userAssignedIdentities -%} + {%- for miResourceId in data.identity.userAssignedIdentities %} '{{miResourceId}}': {} - {% if not loop.last %}{% endif %} + {%- if not loop.last %}{%- endif %} {%- endfor %} } {%- endif %} } {%- endif %} properties: { - {% if data.vnetConfiguration %} + {%- if data.vnetConfiguration %} vnetConfiguration: { internal: {{ data.vnetConfiguration.internal }} infrastructureSubnetId: vnetSubnetId } - {% endif %} + {%- endif %} appLogsConfiguration: { destination: 'azure-monitor' } @@ -69,20 +69,20 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' } } -{% if data.scheduledEntries %} +{%- 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 %} + {%- for entry in data.scheduledEntries %} { weekDay: '{{ entry.weekDay }}', startHourUtc: {{ entry.startHourUtc }}, durationHours: {{ entry.durationHours }} }{{ "," if not loop.last }} - {% endfor %} + {%- endfor %} ] } } -{% endif %} +{%- 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 index e900a399365..b1e25593502 100644 --- a/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/eureka.bicep.j2 @@ -5,12 +5,12 @@ resource eurekaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02 properties: { componentType: 'SpringCloudEureka' configurations: [ - {% for config in data.configurations %} + {%- for config in data.configurations %} { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %}{% endif %} - {% endfor %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} ] scale: { minReplicas: {{ data.replicas }} diff --git a/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 index bfb0c0bb8dc..bd1c067b344 100644 --- a/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/gateway.bicep.j2 @@ -5,35 +5,37 @@ resource gatewayTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-0 properties: { componentType: 'SpringCloudGateway' configurations: [ - {% for config in data.configurations %} + {%- for config in data.configurations %} { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %}{% endif %} - {% endfor %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} ] scale: { minReplicas: {{ data.replicas }} maxReplicas: {{ data.replicas }} } springCloudGatewayRoutes: [ - {% for route in data.routes %} + {%- 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 %} + {%- 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 %} + {%- for filter in route.filters %} + '{{ filter }}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} ] - }{% 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 index 12bcb3ac176..7c54bf4b43c 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -19,7 +19,7 @@ module containerAppEnv 'environment.bicep' = { workloadProfileType: workloadProfileType minNodes: minNodes maxNodes: maxNodes - {% if data.isVnet == true -%} + {%- if data.isVnet == true %} vnetSubnetId: vnetSubnetId {%- 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 index 2e948764549..93df36c5dc1 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -4,13 +4,12 @@ param workloadProfileName = 'Dedicated' param workloadProfileType = 'D4' param minNodes = 1 param maxNodes = 10 - -{% for app in data.apps %} -param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' -param {{app.targetPort}} = 80 -{% endfor %} -{% if data.isVnet == true %} +{%- if data.isVnet == true %} // Provide the full resource ID of the existing subnet. // Example: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} param vnetSubnetId -{% endif %} \ No newline at end of file +{%- endif %} +{%- for app in data.apps %} +param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' +param {{app.targetPort}} = 80 +{%- endfor %} \ 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 index aa74b7e75c1..95a50b403c3 100644 --- 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 @@ -5,12 +5,12 @@ resource sbaTest 'Microsoft.App/managedEnvironments/javaComponents@2024-10-02-pr properties: { componentType: 'SpringBootAdmin' configurations: [ - {% for config in data.configurations %} + {%- for config in data.configurations %} { propertyName: '{{ config.propertyName }}' value: '{{ config.value }}' - }{% if not loop.last %}{% endif %} - {% endfor %} + }{%- if not loop.last %}{%- endif %} + {%- endfor %} ] scale: { minReplicas: {{ data.replicas }} From 516caf504e1407516e710dabc661721a2b8a5c97 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 27 Feb 2025 14:10:25 +0800 Subject: [PATCH 35/98] Refine readme --- .../converter/templates/readme.md.j2 | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index c5050495d93..ed070f24a83 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -1,24 +1,26 @@ -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. Additionally, some configuration updates must be made manually by referencing public Azure documentation after deploying the Bicep files. +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. + +After deploying the Bicep files, some configurations must be updated manually by following the steps outlined in [Azure public documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) after deploying the Bicep files. ## Prerequisites -Before you begin, ensure you have the following: +Before getting started, make sure you have the following: -1. Azure Subscription: An Azure subscription with sufficient permissions to create and manage resources for deploying Azure Container Apps. +1. **Azure Subscription**: An Azure subscription with sufficient permissions to create and manage resources for deploying Azure Container Apps. -2. Bicep CLI: Ensure the Bicep CLI is installed. +2. **Bicep CLI**: Verify that the Bicep CLI is installed. -3. Generated Bicep files: Make sure you have run the following command to generate the Bicep files from your Azure Spring Apps service to Azure Container Apps. +3. **Generated Bicep files**: Ensure you have run the following command to generate the Bicep files from your Azure Spring Apps service to Azure Container Apps. ```azurecli -az spring migration-aca start --service --resource-group --subscription +az spring migration-aca start --service --resource-group --subscription --output-folder ``` ## Steps to Deploy Azure Container Apps Using the Bicep Files -###. Review the Bicep Files and Modify Parameters +### Review the Bicep Files -After running the command to generate the Bicep files, you will have the following files and related resource definitions: +After generating the Bicep files, the specified output directory will contain the following files and related resource definitions: - `main.bicep`: The entry point to deploy and manage Azure Container Apps resources - `param.bicepparam`: The parameters which values required for the deployment. @@ -26,40 +28,67 @@ After running the command to generate the Bicep files, you will have the followi - `-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. -Replace the values in `param.bicepparam` to customize your Azure Container Apps. +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. -###. Customize the Bicep Files +#### VNet configuration -Modify values in the Bicep files as needed to align with your environment. +To migrate a custom virtual network, you must [create the VNet](https://learn.microsoft.com/en-us/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. -- Change the `param.bicepparam` file by filling in the image URL that you get through containerizing your app. Refer to this [doc](https://review.learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-build-overview) for more details about how to containerize your application. -- If you are migrating Application Configuration Service or Config Server, you need to provide the credentials to the remote Git repository. Replace the "*" with the correct credentials in the file `config_server.bicep`. +#### Application Containerization and Deployment +- **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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-build-overview), and learn how to [deploy them](https://learn.microsoft.com/en-us/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/en-us/azure/container-apps/managed-identity-image-pull?tabs=bash&pivots=portal). -### Create a new Resource Group for Azure Container Apps +#### Managed Components Configuration -#### If the resource group you want to deploy the Azure Container Apps does not already exist, create one with the following command: +If you are migrating Application Configuration Service or Config Server, you need to provide the credentials for the remote Git repository in the `config_server.bicep` file. + +#### Mismatch during migration + +During the migration process, you may encounter warnings or errors caused by property mismatches between Azure Spring Apps and Azure Container Apps. If the generated Bicep files don't fully meet your needs, you can customize the resource definitions before deployment. + +### 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 ``` -### Deploy the Bicep files using the Azure CLI: +**Step 2**: Deploy the Bicep files using the following command: ```azurecli az deployment group create --resource-group --template-file main.bicep --parameters param.bicepparam ``` -#### Validate Deployment +**Step 3**: Validate the deployment: -After deployment, validate that all resources, including the Azure Container Apps environment and applications, are correctly provisioned. You can check this in the Azure Portal: +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. Retry the deployment after fixing these issues. +- 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... | Assign Access Control List permissions to system assigned managed identity.​ | + | JavaComponentOperationError | Failed to create config map external-auth-config-map for JavaComponent '' in k8se-system namespace. | Redeploy the bicep files.​ | + | 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. | ## Post-Deployment Configuration -Some properties and configurations are not included in the Bicep files and must be manually updated. Refer to the official Azure documentation for detailed steps: +Some properties and configurations are not included in the Bicep files and must be manually updated. + +- **Custom Domain**: If you have a custom domain configured in Azure Spring Apps, the TLS certificates from Azure Key Vault have been migrated by the migration tool, but you need to follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. +- **Managed Identity**: If you have enbled a system-assigned managed identity in Azure Spring Apps, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal%2Cdotnet) to understand how to use managed identity in Azure Container Apps. +- **Blue-green deployment**: If you 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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. +// todo: add green deployment settings here +- **Auto scale**: If you have enabled auto scaling in Azure Spring Apps, you need to manually configure the differnt auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. -https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview +Refer to the [migration documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) for more detailed feature migration steps. \ No newline at end of file From 58c9560348d6784d713b8b621cbecc3c322dd805 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 27 Feb 2025 18:20:57 +0800 Subject: [PATCH 36/98] gateway issue: route id not unique --- .../azext_spring/migration/converter/gateway_converter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 7c62cc25001..b7e0bdbc6d7 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -47,14 +47,17 @@ def _get_configurations(self, source): def _get_routes(self, routes): aca_routes = [] + name_counter = {} if routes: for route in routes: - aca_id = route['name'].split('/')[-1] + base_name = route['name'].split('/')[-1] 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": aca_id, + "id": f"{base_name}_{count}", "uri": r.get('uri', aca_uri), "predicates": r.get('predicates') if r.get('predicates') else [], "filters": r.get('filters') if r.get('filters') else [], From 519fe95307604c05dd700f8fd60c266319af549c Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Sun, 2 Mar 2025 23:11:05 +0800 Subject: [PATCH 37/98] ext build issues --- src/spring/azext_spring/migration/__init__.py | 4 ++++ .../azext_spring/migration/converter/cert_converter.py | 2 -- src/spring/setup.py | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 src/spring/azext_spring/migration/__init__.py 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/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 9f141eb0ab6..100492e5db6 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -6,7 +6,6 @@ class CertConverter(ConverterTemplate): def load_source(self, source): self.source = source - print(f"Cert source: {self.source}") def calculate_data(self): certName = self.source['name'].split('/')[-1] @@ -27,7 +26,6 @@ def calculate_data(self): self.data["value"] = "*" isKeyVaultCert = False self.data["isKeyVaultCert"] = isKeyVaultCert - print(f"cert data: {self.data}") def get_template_name(self): return "cert.bicep" diff --git a/src/spring/setup.py b/src/spring/setup.py index bf76d1caacb..31d58d59849 100644 --- a/src/spring/setup.py +++ b/src/spring/setup.py @@ -53,5 +53,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'] + }, ) From 9d2d0e2c1d3859280557a6668300accbe571b7b8 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 3 Mar 2025 11:26:06 +0800 Subject: [PATCH 38/98] Add dependency jinja2 --- src/spring/setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spring/setup.py b/src/spring/setup.py index 31d58d59849..46df61892c5 100644 --- a/src/spring/setup.py +++ b/src/spring/setup.py @@ -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() From 915a8719c5c857bc7e5ef53ee35cf6aee852fa94 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Mon, 3 Mar 2025 16:33:50 +0800 Subject: [PATCH 39/98] add message guide in readme --- .../converter/templates/readme.md.j2 | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index ed070f24a83..3c868fceb9b 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -18,6 +18,22 @@ az spring migration-aca start --service --resou ## Steps to Deploy Azure Container Apps Using the Bicep Files +### Check Output Message + +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 port 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." + ``` + ### Review the Bicep Files After generating the Bicep files, the specified output directory will contain the following files and related resource definitions: @@ -65,6 +81,14 @@ az group create --name --location az deployment group create --resource-group --template-file main.bicep --parameters param.bicepparam ``` +**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. @@ -85,10 +109,9 @@ You can check the deployment status either in the CLI output or in the Azure Por Some properties and configurations are not included in the Bicep files and must be manually updated. - **Custom Domain**: If you have a custom domain configured in Azure Spring Apps, the TLS certificates from Azure Key Vault have been migrated by the migration tool, but you need to follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. -- **Managed Identity**: If you have enbled a system-assigned managed identity in Azure Spring Apps, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal%2Cdotnet) to understand how to use managed identity in Azure Container Apps. +- **Managed Identity**: If you have enabled a system-assigned managed identity in Azure Spring Apps, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal%2Cdotnet) to understand how to use managed identity in Azure Container Apps. - **Blue-green deployment**: If you 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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. -// todo: add green deployment settings here -- **Auto scale**: If you have enabled auto scaling in Azure Spring Apps, you need to manually configure the differnt auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. +- **Auto scale**: If you have enabled auto scaling in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. Refer to the [migration documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) for more detailed feature migration steps. \ No newline at end of file From 884488f840402822a272e2d05f63e833f7742bf2 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 3 Mar 2025 16:36:11 +0800 Subject: [PATCH 40/98] format --- .../migration/converter/templates/main.bicep.j2 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 7c54bf4b43c..bd5abe58379 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -13,7 +13,7 @@ param {{app.targetPort}} int {%- endfor %} module containerAppEnv 'environment.bicep' = { - name: 'containerAppEnvDeployment' + name: 'container-app-environment-Deployment' params: { workloadProfileName: workloadProfileName workloadProfileType: workloadProfileType @@ -27,7 +27,7 @@ module containerAppEnv 'environment.bicep' = { {%- for item in data.certs %} module {{ item.moduleName }} '{{ item.templateName }}' = { - name: 'Cert{{ item.certName }}-Deployment' + name: 'cert-{{ item.certName }}-Deployment' dependsOn: [ containerAppEnv ] @@ -54,7 +54,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {%- if data.gateway == true %} module managedGateway 'gateway.bicep' = { - name: 'gatewayDeployment' + name: 'gateway-Deployment' dependsOn: [ containerAppEnv ] @@ -66,7 +66,7 @@ module managedGateway 'gateway.bicep' = { {%- if data.config == true %} module managedConfig 'config_server.bicep' = { - name: 'configServerDeployment' + name: 'config-server-Deployment' dependsOn: [ containerAppEnv ] @@ -78,7 +78,7 @@ module managedConfig 'config_server.bicep' = { {%- if data.eureka == true %} module managedEureka 'eureka.bicep' = { - name: 'eurekaDeployment' + name: 'eureka-Deployment' dependsOn: [ containerAppEnv ] @@ -90,7 +90,7 @@ module managedEureka 'eureka.bicep' = { {%- if data.sba == true %} module managedSpringBootAdmin 'spring_boot_admin.bicep' = { - name: 'springBootAdminDeployment' + name: 'spring-boot-admin-Deployment' dependsOn: [ containerAppEnv ] From 6b241ad5dbf780f142ba6181280f475aaf3e9058 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Mon, 3 Mar 2025 17:41:34 +0800 Subject: [PATCH 41/98] byos migration --- .../migration/converter/app_converter.py | 33 ++++++++++++++- .../migration/converter/base_converter.py | 12 ++++++ .../migration/converter/cert_converter.py | 4 +- .../migration/converter/conversion_context.py | 6 ++- .../converter/environment_converter.py | 40 ++++++++++++++++++- .../migration/converter/main_converter.py | 21 ++++++++-- .../migration/converter/param_converter.py | 20 ++++++++-- .../converter/templates/app.bicep.j2 | 22 ++++++++++ .../templates/config_server.bicep.j2 | 5 ++- .../converter/templates/environment.bicep.j2 | 19 +++++++++ .../converter/templates/main.bicep.j2 | 6 +++ .../converter/templates/param.bicepparam.j2 | 6 +++ 12 files changed, 180 insertions(+), 14 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 08ff696098a..1e2e934c218 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -7,6 +7,9 @@ # Concrete Converter Subclass for Container App class AppConverter(ConverterTemplate): + + DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" + def load_source(self, source): self.source = source self.managed_components = source['managedComponents'] @@ -26,7 +29,33 @@ def calculate_data(self): isPublic = self.source['properties'].get('public') identity = self.source.get('identity') # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") - + volumeMounts = [] + volumes = [] + if 'properties' in self.source and 'customPersistentDisks' in self.source['properties']: + disks = self.source['properties']['customPersistentDisks'] + for disk_props in disks: + print(f"Disk props: {disk_props}") + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + # print("Storage name: ", storage_name) + 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 += option + ("," if not mountOptions else "") + print("Mount options: ", mountOptions) + volumeMounts.append({ + "volumeName": storage_name, + "mountPath": disk_props.get('customPersistentDiskProperties').get('mountPath'), + }) + volumes.append({ + "volumeName": storage_name, + "storageName": appName + "-" + storage_name, + "mountOptions": mountOptions, + }) + # print("Volume mounts: ", volumeMounts) + # print("Volumes: ", volumes) self.data = { "containerAppName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), @@ -41,6 +70,8 @@ def calculate_data(self): "green": greenDeployment, "isBlueGreen": len(deployments) > 1, "identity": identity, + "volumeMounts": volumeMounts, + "volumes": volumes, } def get_template_name(self): diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index ef21b63f827..119766e232b 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -42,3 +42,15 @@ def generate_output(self): with open(f"{script_dir}/templates/{template_name}.j2") as file: template = Template(file.read()) return template.render(data=self.data, params=self.params) + + # 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_resource_name(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 + return params[-1].strip().strip("'") if params else '' \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 9f141eb0ab6..bb856b7be09 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -6,7 +6,7 @@ class CertConverter(ConverterTemplate): def load_source(self, source): self.source = source - print(f"Cert source: {self.source}") + # print(f"Cert source: {self.source}") def calculate_data(self): certName = self.source['name'].split('/')[-1] @@ -27,7 +27,7 @@ def calculate_data(self): self.data["value"] = "*" isKeyVaultCert = False self.data["isKeyVaultCert"] = isKeyVaultCert - print(f"cert data: {self.data}") + # print(f"cert data: {self.data}") def get_template_name(self): return "cert.bicep" diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 58fa3f9495e..34457cc60d1 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -42,13 +42,15 @@ def run_converters(self, source): source_wrapper = SourceDataWrapper(source) asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + storages = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') # Environment Converter is_vnet = self._is_vnet(asa_service) is_enterprise = self._is_enterprise_tier(asa_service) asa_service['isVnet'] = is_vnet asa_service['apps'] = asa_apps - converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert(asa_service) + asa_service['storages'] = storages + # Cert Converter asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') @@ -60,7 +62,7 @@ def run_converters(self, source): converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) elif cert['properties'].get('type') == "ContentCertificate": converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) - + converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert(asa_service) # Managed components Converter managed_components = { 'gateway': False, diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 299839eee5b..c64261f91a9 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -8,13 +8,15 @@ def load_source(self, source): def calculate_data(self): name = self.source['name'].split('/')[-1] apps = self.source.get('apps') + storages = self.source.get('storages') self.data = { "containerAppEnvName": name, "containerAppLogAnalyticsName": f"log-{name}", "identity": { "type": self._get_identity_type(apps), "userAssignedIdentities": self._get_user_assigned_identity_list(apps), - } + }, + "storages": self._get_app_storage_configs(apps, storages), } isVnet = self.source['isVnet'] @@ -67,3 +69,39 @@ def _get_user_assigned_identity_list(self, apps): for id in app['identity']['userAssignedIdentities']: user_assigned_identities.append(id) return user_assigned_identities + + def _get_app_storage_configs(self, apps, storages): + storage_configs = [] + + # Create a mapping of storage IDs to account names + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in storages + } + # print("storage_map:", storage_map) + for app in apps: + # Check if app has properties and customPersistentDiskProperties + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] + for disk_props in disks: + # Get the account name from storage map using storageId + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + account_name = storage_map.get(storage_name, '') + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + app_name = app['name'].split('/')[-1] + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + (app_name + "_" + storage_name).replace("-", "") + storage_config = { + 'containerAppEnvStorageName': containerAppEnvStorageName, + 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + 'storageName': app_name + "-" + storage_name, + 'shareName': share_name, + 'accessMode': access_mode, + 'accountName': account_name, + } + storage_configs.append(storage_config) + # print("storage_configs:", storage_configs) + return storage_configs diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index c6d2b6641eb..dc94c664093 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -23,8 +23,9 @@ def calculate_data(self): self.data["certs"].append(certData) self.data.setdefault("apps", []) - for item in self.apps: - appName = item['name'].split('/')[-1] + storage_configs = [] + for app in self.apps: + appName = app['name'].split('/')[-1] moduleName = appName.replace("-", "_") templateName = f"{appName}_app.bicep" appData = { @@ -34,10 +35,24 @@ def calculate_data(self): "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), "targetPort": "targetPortFor_"+appName.replace("-", "_"), } + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] + for disk_props in disks: + # Get the account name from storage map using storageId + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + app_name = app['name'].split('/')[-1] + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + (app_name + "_" + storage_name).replace("-", "") + storage_config = { + 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + } + storage_configs.append(storage_config) + self.data["apps"].append(appData) + self.data["storages"] = storage_configs for name, value in self.managedComponents.items(): self.data[name] = value def get_template_name(self): - return "main.bicep" \ No newline at end of file + return "main.bicep" diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 09c1bc82139..9021e7a539b 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -8,14 +8,28 @@ def load_source(self, source): def calculate_data(self): self.data.setdefault("apps", []) - for item in self.apps: - appName = item['name'].split('/')[-1] + storage_configs = [] + for app in self.apps: + appName = app['name'].split('/')[-1] self.data["apps"].append({ "appName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), "targetPort": "targetPortFor_"+appName.replace("-", "_"), }) + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] + for disk_props in disks: + # Get the account name from storage map using storageId + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + app_name = app['name'].split('/')[-1] + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + (app_name + "_" + storage_name).replace("-", "") + storage_config = { + 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + } + storage_configs.append(storage_config) + self.data["storages"] = storage_configs self.data["isVnet"] = self.is_vnet def get_template_name(self): - return "param.bicepparam" \ No newline at end of file + return "param.bicepparam" diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index 7ec47fdc18a..d50a0b21a47 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -171,8 +171,30 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { } {%- 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 %} { 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 index e260c3f8cb1..32e44872ef0 100644 --- a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -1,10 +1,11 @@ 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 = 'username' +// Example: param spring_cloud_config_server_git_username string = 'username' {%- for param in data.params %} -param {{param}} +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 }}' diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 666f10ae0b1..dd4e3cab612 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -84,5 +84,24 @@ resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigu } {%- endif %} +{%- if data.storages %} +{%- for storage in data.storages %} + +param {{storage.containerAppEnvStorageAccountKey}} string +resource {{storage.containerAppEnvStorageName}} '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.containerAppEnvStorageAccountKey}} + } + } +} +{%- endfor %} +{%- endif %} + output containerAppEnvId string = containerAppEnv.id output containerAppEnvName string = containerAppEnv.name diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 7c54bf4b43c..1e7da1bd5a3 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -11,6 +11,9 @@ param vnetSubnetId string param {{app.containerAppImageName}} string param {{app.targetPort}} int {%- endfor %} +{%- for storage in data.storages %} +param {{storage.containerAppEnvStorageAccountKey}} string +{%- endfor %} module containerAppEnv 'environment.bicep' = { name: 'containerAppEnvDeployment' @@ -22,6 +25,9 @@ module containerAppEnv 'environment.bicep' = { {%- if data.isVnet == true %} vnetSubnetId: vnetSubnetId {%- endif %} + {%- for storage in data.storages %} + {{storage.containerAppEnvStorageAccountKey}}: {{storage.containerAppEnvStorageAccountKey}} + {%- endfor %} } } diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 93df36c5dc1..935a60a2d36 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -4,12 +4,18 @@ 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. // Example: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} param vnetSubnetId {%- endif %} + {%- for app in data.apps %} param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' param {{app.targetPort}} = 80 +{%- endfor %} + +{%- for storage in data.storages %} +param {{storage.containerAppEnvStorageAccountKey}} = 'fill in storage account key' {%- endfor %} \ No newline at end of file From 325e4233ffa98eb48fea70f02c9f0449cc1419ea Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Wed, 5 Mar 2025 01:12:09 +0800 Subject: [PATCH 42/98] fix byos duplicate name issue --- .../migration/converter/app_converter.py | 21 ++++++++++++++----- .../migration/converter/base_converter.py | 11 +++++++++- .../migration/converter/conversion_context.py | 2 ++ .../converter/environment_converter.py | 8 +++++-- .../migration/converter/main_converter.py | 15 +++++++++++-- .../migration/converter/param_converter.py | 15 ++++++++++++- .../converter/templates/param.bicepparam.j2 | 2 +- 7 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 1e2e934c218..e1417646e91 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -28,13 +28,18 @@ def calculate_data(self): ingress = self._get_ingress(self.source, tier) isPublic = self.source['properties'].get('public') identity = self.source.get('identity') + storages = self.source.get('storages') # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") volumeMounts = [] volumes = [] if 'properties' in self.source and 'customPersistentDisks' in self.source['properties']: disks = self.source['properties']['customPersistentDisks'] + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in storages + } for disk_props in disks: - print(f"Disk props: {disk_props}") + # print(f"Disk props: {disk_props}") storage_id = disk_props.get('storageId', '') storage_name = self._get_resource_name(storage_id) if storage_id else '' # print("Storage name: ", storage_name) @@ -43,15 +48,21 @@ def calculate_data(self): len(disk_props.get('customPersistentDiskProperties').get('mountOptions')) > 0: mountOptions = "" for option in disk_props.get('customPersistentDiskProperties').get('mountOptions'): - mountOptions += option + ("," if not mountOptions else "") - print("Mount options: ", mountOptions) + mountOptions += ("," if mountOptions != "" else "") + option + account_name = storage_map.get(storage_name, '') + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + # print("Mount options: ", mountOptions) volumeMounts.append({ "volumeName": storage_name, - "mountPath": disk_props.get('customPersistentDiskProperties').get('mountPath'), + "mountPath": mount_path, }) volumes.append({ "volumeName": storage_name, - "storageName": appName + "-" + storage_name, + "storageName": storage_unique_name, "mountOptions": mountOptions, }) # print("Volume mounts: ", volumeMounts) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 119766e232b..5ab562f687d 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -1,3 +1,4 @@ +import hashlib import os from abc import ABC, abstractmethod @@ -53,4 +54,12 @@ def _get_resource_name(self, resource_id): # Split by comma and get the last parameter params = content.split(',') # Return the last parameter stripped of quotes and whitespace - return params[-1].strip().strip("'") if params else '' \ No newline at end of file + result = params[-1].strip().strip("'") if params else '' + # print(f"Resource name: {result}") + return result + + def _get_storage_unique_name(self, storage_name, account_name, share_name, mount_path, access_mode): + 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}" + return result[:32] # Ensure total length is no more than 32 \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 34457cc60d1..0d238cbfbfb 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -81,6 +81,7 @@ def run_converters(self, source): app_source['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app_source['name']}/")] app_source['managedComponents'] = managed_components app_source['isEnterprise'] = is_enterprise + app_source['storages'] = storages converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app_source) # Param, readme and main Converter @@ -91,6 +92,7 @@ def run_converters(self, source): "managedComponents": managed_components, "isVnet": is_vnet, "inEnterprise": is_enterprise, + "storages": storages, } converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(full_source) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index c64261f91a9..efa4f33cc36 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -92,12 +92,16 @@ def _get_app_storage_configs(self, apps, storages): app_name = app['name'].split('/')[-1] readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) + storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + (app_name + "_" + storage_name).replace("-", "") + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + # print("storage_unique_name:", storage_unique_name) storage_config = { 'containerAppEnvStorageName': containerAppEnvStorageName, 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, - 'storageName': app_name + "-" + storage_name, + 'storageName': storage_unique_name, 'shareName': share_name, 'accessMode': access_mode, 'accountName': account_name, diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index dc94c664093..aa7922f9677 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -7,6 +7,7 @@ def load_source(self, source): self.apps = source["apps"] self.managedComponents = source["managedComponents"] self.certs = source["certs"] + self.storages = source["storages"] def calculate_data(self): self.data["isVnet"] = self.source.get("isVnet", False) @@ -21,7 +22,10 @@ def calculate_data(self): "templateName": templateName, } self.data["certs"].append(certData) - + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in self.storages + } self.data.setdefault("apps", []) storage_configs = [] for app in self.apps: @@ -42,7 +46,14 @@ def calculate_data(self): storage_id = disk_props.get('storageId', '') storage_name = self._get_resource_name(storage_id) if storage_id else '' app_name = app['name'].split('/')[-1] - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + (app_name + "_" + storage_name).replace("-", "") + account_name = storage_map.get(storage_name, '') + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + # print("storage_unique_name:", storage_unique_name) + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name storage_config = { 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, } diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 9021e7a539b..3aa4fc674e3 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -5,8 +5,13 @@ class ParamConverter(ConverterTemplate): def load_source(self, source): self.apps = source['apps'] self.is_vnet = source['isVnet'] + self.storages = source['storages'] def calculate_data(self): + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in self.storages + } self.data.setdefault("apps", []) storage_configs = [] for app in self.apps: @@ -23,9 +28,17 @@ def calculate_data(self): storage_id = disk_props.get('storageId', '') storage_name = self._get_resource_name(storage_id) if storage_id else '' app_name = app['name'].split('/')[-1] - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + (app_name + "_" + storage_name).replace("-", "") + account_name = storage_map.get(storage_name, '') + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + # print("storage_unique_name:", storage_unique_name) storage_config = { 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + 'accountName': account_name, } storage_configs.append(storage_config) self.data["storages"] = storage_configs diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 935a60a2d36..fa4142aa1ee 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -17,5 +17,5 @@ param {{app.targetPort}} = 80 {%- endfor %} {%- for storage in data.storages %} -param {{storage.containerAppEnvStorageAccountKey}} = 'fill in storage account key' +param {{storage.containerAppEnvStorageAccountKey}} = 'fill in account key for storage "{{storage.accountName}}"' {%- endfor %} \ No newline at end of file From 084f952f1698757dd0bc62a7ea13e746fbb3396b Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 3 Mar 2025 16:36:11 +0800 Subject: [PATCH 43/98] format --- .../migration/converter/templates/main.bicep.j2 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 1e7da1bd5a3..51ca387e3d3 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -16,7 +16,7 @@ param {{storage.containerAppEnvStorageAccountKey}} string {%- endfor %} module containerAppEnv 'environment.bicep' = { - name: 'containerAppEnvDeployment' + name: 'container-app-environment-Deployment' params: { workloadProfileName: workloadProfileName workloadProfileType: workloadProfileType @@ -33,7 +33,7 @@ module containerAppEnv 'environment.bicep' = { {%- for item in data.certs %} module {{ item.moduleName }} '{{ item.templateName }}' = { - name: 'Cert{{ item.certName }}-Deployment' + name: 'cert-{{ item.certName }}-Deployment' dependsOn: [ containerAppEnv ] @@ -60,7 +60,7 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {%- if data.gateway == true %} module managedGateway 'gateway.bicep' = { - name: 'gatewayDeployment' + name: 'gateway-Deployment' dependsOn: [ containerAppEnv ] @@ -72,7 +72,7 @@ module managedGateway 'gateway.bicep' = { {%- if data.config == true %} module managedConfig 'config_server.bicep' = { - name: 'configServerDeployment' + name: 'config-server-Deployment' dependsOn: [ containerAppEnv ] @@ -84,7 +84,7 @@ module managedConfig 'config_server.bicep' = { {%- if data.eureka == true %} module managedEureka 'eureka.bicep' = { - name: 'eurekaDeployment' + name: 'eureka-Deployment' dependsOn: [ containerAppEnv ] @@ -96,7 +96,7 @@ module managedEureka 'eureka.bicep' = { {%- if data.sba == true %} module managedSpringBootAdmin 'spring_boot_admin.bicep' = { - name: 'springBootAdminDeployment' + name: 'spring-boot-admin-Deployment' dependsOn: [ containerAppEnv ] From ca308431734a7a7c26b433c7d364b5131e181243 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 02:59:46 +0800 Subject: [PATCH 44/98] refactor, configserver, acs and app --- .../migration/converter/acs_converter.py | 34 ++++----- .../migration/converter/app_converter.py | 58 +++++++++------- .../migration/converter/base_converter.py | 69 ++++++++++++++++--- .../migration/converter/cert_converter.py | 7 ++ .../converter/config_server_converter.py | 36 +++++----- .../migration/converter/conversion_context.py | 62 ++++------------- .../converter/environment_converter.py | 10 ++- .../migration/converter/eureka_converter.py | 7 +- .../migration/converter/gateway_converter.py | 7 +- .../converter/live_view_converter.py | 7 +- .../migration/converter/main_converter.py | 29 +++++--- .../migration/converter/param_converter.py | 19 +++-- .../migration/converter/readme_converter.py | 7 ++ .../converter/service_registry_converter.py | 7 +- .../migration/migration_operations.py | 26 +++---- 15 files changed, 230 insertions(+), 155 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 144b5c2d7b0..2036be4c6a9 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -14,32 +14,28 @@ class ACSConverter(ConverterTemplate): KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm" KEY_PATTERN = ".pattern" - def __init__(self): - super().__init__() - - def load_source(self, source): - self.source = source - - def calculate_data(self): - name = f"config" - configurations, params = self._get_configurations_and_params(self.source) - replicas = 2 - - self.data = { - "configServerName": name, - "params": params, - "configurations": configurations, - "replicas": replicas - } + def __init__(self, input): + def extract_data(input): + acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0] + name = f"config" + configurations, params = self._get_configurations_and_params(acs) + replicas = 2 + return { + "configServerName": name, + "params": params, + "configurations": configurations, + "replicas": replicas + } + super().__init__(input, extract_data) def get_template_name(self): return "config_server.bicep" - def _get_configurations_and_params(self, source): + def _get_configurations_and_params(self, acs): configurations = [] params = [] - git_repos = source.get('properties', {}).get('settings', {}).get('gitProperty', {}).get('repositories') + git_repos = acs.get('properties', {}).get('settings', {}).get('gitProperty', {}).get('repositories') 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')) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index e1417646e91..013b830da5b 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -1,7 +1,8 @@ +import os import re - from knack.log import get_logger from .base_converter import ConverterTemplate +from jinja2 import Template logger = get_logger(__name__) @@ -10,30 +11,38 @@ class AppConverter(ConverterTemplate): DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" - def load_source(self, source): - self.source = source - self.managed_components = source['managedComponents'] - self.is_enterprise = source['isEnterprise'] - # print(f"App source: {self.source}") + def __init__(self, input): + def extract_data(input): + return self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + super().__init__(input, extract_data) + + def convert2(self): + outputs = {} + for app in self.data: + appName = app['name'].split('/')[-1] + appData = self.transform_data(app) + outputs[appName+"_"+self.get_template_name()] = self.generate_output(appData) + return outputs - def calculate_data(self): - appName = self.source['name'].split('/')[-1] - envName = self.source['name'].split('/')[0] + def transform_data(self, app): + appName = app['name'].split('/')[-1] + envName = app['name'].split('/')[0] + asa_deployments = self.wrapper_data.get_deployments_by_app(app['name']) moduleName = appName.replace("-", "_") - serviceBinds = self._get_service_bind(self.source, envName) - deployments = self._get_deployments(self.source) + serviceBinds = self._get_service_bind(app, envName) + deployments = self._get_deployments(asa_deployments) blueDeployment = deployments[0] if len(deployments) > 0 else {} greenDeployment = deployments[1] if len(deployments) > 1 else {} tier = blueDeployment.get('sku', {}).get('tier') - ingress = self._get_ingress(self.source, tier) - isPublic = self.source['properties'].get('public') - identity = self.source.get('identity') - storages = self.source.get('storages') + ingress = self._get_ingress(app, tier) + isPublic = app['properties'].get('public') + identity = app.get('identity') + storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") volumeMounts = [] volumes = [] - if 'properties' in self.source and 'customPersistentDisks' in self.source['properties']: - disks = self.source['properties']['customPersistentDisks'] + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] storage_map = { storage['name'].split('/')[-1]: storage['properties']['accountName'] for storage in storages @@ -67,7 +76,7 @@ def calculate_data(self): }) # print("Volume mounts: ", volumeMounts) # print("Volumes: ", volumes) - self.data = { + return { "containerAppName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), "targetPort": "targetPortFor_"+appName.replace("-", "_"), @@ -83,7 +92,7 @@ def calculate_data(self): "identity": identity, "volumeMounts": volumeMounts, "volumes": volumes, - } + } def get_template_name(self): return "app.bicep" @@ -92,7 +101,6 @@ def get_app_name(input_string): return input_string.split('/')[-1] def _get_service_bind(self, source, envName): - enable_sba = self.managed_components['sba'] service_bind = [] addon = source['properties'].get('addonConfigs') @@ -105,7 +113,7 @@ def _get_service_bind(self, source, envName): "name": "bind-config", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" }) - if self.is_enterprise != True and self.managed_components['config'] == True: + if self.wrapper_data.is_enterprise_tier() != True and self.wrapper_data.is_support_configserver(): # standard tier enabled config server and bind all apps automatically service_bind.append({ "name": "bind-config", @@ -116,13 +124,13 @@ def _get_service_bind(self, source, envName): "name": "bind-eureka", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" }) - if self.is_enterprise != True and self.managed_components['eureka'] == True: + if self.wrapper_data.is_enterprise_tier() != 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 enable_sba: + if self.wrapper_data.is_support_sba(): service_bind.append({ "name": "bind-sba", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'admin')" @@ -130,9 +138,9 @@ def _get_service_bind(self, source, envName): # print(f"Service bind: {service_bind}") return service_bind - def _get_deployments(self, source): + def _get_deployments(self, asa_deployments): deployments = [] - for deployment in source['deployments']: + for deployment in asa_deployments: deployment_name = deployment['name'].split('/')[-1] env = deployment.get('properties', {}).get('deploymentSettings', {}).get('environmentVariables', {}) liveness_probe = deployment.get('properties', {}).get('deploymentSettings', {}).get('livenessProbe', {}) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 5ab562f687d..9c73c290ffb 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -12,24 +12,27 @@ # 2. Calculate the output data # 3. Generate the output data class ConverterTemplate(ABC): - def __init__(self): + def __init__(self, input, extract_data): self.params = {} # custom facing parameters for the converter self.data = {} # output data of the converter self.source = {} # input data of the converter + self.wrapper_data = SourceDataWrapper(input) + self.data = extract_data(input=None) + def set_params(self, params): self.params = params - def convert(self, source): + def convert(self, source=None): self.load_source(source) self.calculate_data() - return self.generate_output() + return self.generate_output(self.data) - @abstractmethod + # @abstractmethod # TODO: To be removed def load_source(self, source): # load the input data pass - @abstractmethod + # @abstractmethod # TODO: To be removed def calculate_data(self): # calculate the output data pass @@ -37,12 +40,12 @@ def calculate_data(self): # calculate the output data def get_template_name(self): pass - def generate_output(self): + def generate_output(self, data): script_dir = os.path.dirname(os.path.abspath(__file__)) template_name = self.get_template_name() with open(f"{script_dir}/templates/{template_name}.j2") as file: template = Template(file.read()) - return template.render(data=self.data, params=self.params) + return template.render(data=data, params=self.params) # Extracts the resource name from a resource ID string in Azure ARM template format # Format: [resourceId('Microsoft.AppPlatform/Spring/', '', '')] @@ -62,4 +65,54 @@ def _get_storage_unique_name(self, storage_name, account_name, share_name, mount 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}" - return result[:32] # Ensure total length is no more than 32 \ No newline at end of file + return result[:32] # Ensure total length is no more than 32 + +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_ssoconfigserver() or self.is_support_acs() + + def is_support_ssoconfigserver(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/configServers') + + def is_support_acs(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/configurationServices') + + def is_support_eureka(self): + return self.is_support_feature('Microsoft.AppPlatform/Spring/serviceRegistries') + + 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/routeConfigs') + + 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_name): + deployments = self.get_deployments() + return [deployment for deployment in deployments if deployment['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 \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index bb856b7be09..0a3e3d6d9dd 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -4,6 +4,13 @@ # Concrete Converter Subclass for certificate class CertConverter(ConverterTemplate): + + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) + def load_source(self, source): self.source = source # print(f"Cert source: {self.source}") diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index 3f612da2efb..361e7620e10 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -1,5 +1,7 @@ from .base_converter import ConverterTemplate +from knack.log import get_logger +logger = get_logger(__name__) # Concrete Converter Subclass for Config Server class ConfigServerConverter(ConverterTemplate): @@ -14,32 +16,28 @@ class ConfigServerConverter(ConverterTemplate): KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm" KEY_PATTERN = ".pattern" - def __init__(self): - super().__init__() - - def load_source(self, source): - self.source = source - - def calculate_data(self): - name = f"config" - configurations, params = self._get_configurations_and_params(self.source) - replicas = 2 - - self.data = { - "configServerName": name, - "params": params, - "configurations": configurations, - "replicas": replicas - } + def __init__(self, input): + def extract_data(input): + configServer = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers')[0] + name = f"config" + configurations, params = self._get_configurations_and_params(configServer) + replicas = 2 + return { + "configServerName": name, + "params": params, + "configurations": configurations, + "replicas": replicas + } + super().__init__(input, extract_data) def get_template_name(self): return "config_server.bicep" - def _get_configurations_and_params(self, source): + def _get_configurations_and_params(self, configServer): configurations = [] params = [] - git_property = source.get('properties', {}).get('configServer', {}).get('gitProperty') + 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')) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 0d238cbfbfb..793b8e28fa9 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from knack.log import get_logger -from .base_converter import ConverterTemplate +from .base_converter import ConverterTemplate, SourceDataWrapper from .environment_converter import EnvironmentConverter from .app_converter import AppConverter from .readme_converter import ReadMeConverter @@ -20,7 +20,8 @@ # Context Class class ConversionContext: - def __init__(self): + def __init__(self, input): + self.data_wrapper = SourceDataWrapper(input) self.converters = [] def add_converter(self, converter: ConverterTemplate): @@ -45,9 +46,6 @@ def run_converters(self, source): storages = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') # Environment Converter - is_vnet = self._is_vnet(asa_service) - is_enterprise = self._is_enterprise_tier(asa_service) - asa_service['isVnet'] = is_vnet asa_service['apps'] = asa_apps asa_service['storages'] = storages @@ -71,18 +69,18 @@ def run_converters(self, source): 'sba': False, } converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) - converted_contents = self._convert_config_server_and_ACS(source_wrapper, converted_contents, managed_components) + + if self.data_wrapper.is_support_ssoconfigserver(): + converted_contents[self.get_converter(ConfigServerConverter).get_template_name()] = self.get_converter(ConfigServerConverter).convert() + logger.debug(f"converted_contents for config server: {converted_contents[self.get_converter(ConfigServerConverter).get_template_name()]}") + elif self.data_wrapper.is_support_acs(): + converted_contents[self.get_converter(ACSConverter).get_template_name()] = self.get_converter(ACSConverter).convert() + logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[self.get_converter(ACSConverter).get_template_name()]}") + converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) - asa_deployments = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') - for app_source in asa_apps: - appName = app_source['name'].split('/')[-1] - app_source['deployments'] = [deployment for deployment in asa_deployments if deployment['name'].startswith(f"{app_source['name']}/")] - app_source['managedComponents'] = managed_components - app_source['isEnterprise'] = is_enterprise - app_source['storages'] = storages - converted_contents[appName+"_"+self.get_converter(AppConverter).get_template_name()] = self.get_converter(AppConverter).convert(app_source) + converted_contents.update(self.get_converter(AppConverter).convert2()) # Param, readme and main Converter full_source = { @@ -90,8 +88,6 @@ def run_converters(self, source): "apps": asa_apps, "certs": asa_kv_certs, "managedComponents": managed_components, - "isVnet": is_vnet, - "inEnterprise": is_enterprise, "storages": storages, } @@ -126,25 +122,6 @@ def _convert_gateway(self, source_wrapper, converted_contents, managed_component logger.info(f"converted_contents for gateway: {converted_contents[gateway_key]}") return converted_contents - def _convert_config_server_and_ACS(self, source_wrapper, converted_contents, managed_components): - enabled_config_server = False - - for config_server in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers'): - managed_components['config'] = True - enabled_config_server = True - config_key = self.get_converter(ConfigServerConverter).get_template_name() - converted_contents[config_key] = self.get_converter(ConfigServerConverter).convert(config_server) - logger.debug(f"converted_contents for config server: {converted_contents[config_key]}") - - if not enabled_config_server: - for acs in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices'): - managed_components['config'] = True - config_key = self.get_converter(ACSConverter).get_template_name() - converted_contents[config_key] = self.get_converter(ACSConverter).convert(acs) - logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[config_key]}") - - return converted_contents - def _convert_live_view(self, source_wrapper, converted_contents, managed_components): for live_view in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews'): managed_components['sba'] = True @@ -154,7 +131,7 @@ def _convert_live_view(self, source_wrapper, converted_contents, managed_compone return converted_contents def _convert_eureka_and_service_registry(self, source_wrapper, converted_contents, asa_service, managed_components): - is_enterprise_tier = self._is_enterprise_tier(asa_service) + is_enterprise_tier = self.is_enterprise_tier(asa_service) for service_registry in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries'): managed_components['eureka'] = True eureka_key = self.get_converter(ServiceRegistryConverter).get_template_name() @@ -168,18 +145,7 @@ def _convert_eureka_and_service_registry(self, source_wrapper, converted_content converted_contents[eureka_key] = self.get_converter(EurekaConverter).convert(None) return converted_contents - def _is_enterprise_tier(self, asa_service): + def is_enterprise_tier(self, asa_service): return asa_service['sku']['tier'] == 'Enterprise' - def _is_vnet(self, asa_service): - networkProfile = asa_service['properties'].get('networkProfile') - if networkProfile is None: - return False - return networkProfile.get('appSubnetId') is not None - -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] diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index efa4f33cc36..ac8ed77fb6b 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -2,6 +2,13 @@ # Concrete Subclass for Container App Environment class EnvironmentConverter(ConverterTemplate): + + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) + def load_source(self, source): self.source = source @@ -19,8 +26,7 @@ def calculate_data(self): "storages": self._get_app_storage_configs(apps, storages), } - isVnet = self.source['isVnet'] - if isVnet: + if self.wrapper_data.is_vnet(): self.data["vnetConfiguration"] = { "internal": str(True).lower(), } diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 08ce3b6b218..adc4ad0b278 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -2,8 +2,11 @@ class EurekaConverter(ConverterTemplate): - def __init__(self): - super().__init__() + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) def load_source(self, source): self.source = source diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index b7e0bdbc6d7..8b67de4530c 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -3,8 +3,11 @@ class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" - def __init__(self, client, resource_group, service): - super().__init__() + def __init__(self, input, client, resource_group, service): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) self.client = client self.resource_group = resource_group self.service = service diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 9d4a9500415..79b09360ed5 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -2,8 +2,11 @@ class LiveViewConverter(ConverterTemplate): - def __init__(self): - super().__init__() + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) def load_source(self, source): self.source = source diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index aa7922f9677..019ba928f7e 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -2,6 +2,13 @@ # Concrete Converter Subclass for Read Me class MainConverter(ConverterTemplate): + + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) + def load_source(self, source): self.source = source self.apps = source["apps"] @@ -10,8 +17,7 @@ def load_source(self, source): self.storages = source["storages"] def calculate_data(self): - self.data["isVnet"] = self.source.get("isVnet", False) - self.data.setdefault("certs", []) + certs = [] for item in self.certs: certName = item['name'].split('/')[-1] moduleName = "cert_" + certName.replace("-", "_") @@ -21,13 +27,13 @@ def calculate_data(self): "moduleName": moduleName, "templateName": templateName, } - self.data["certs"].append(certData) + certs.append(certData) storage_map = { storage['name'].split('/')[-1]: storage['properties']['accountName'] for storage in self.storages } - self.data.setdefault("apps", []) storage_configs = [] + apps_data = [] for app in self.apps: appName = app['name'].split('/')[-1] moduleName = appName.replace("-", "_") @@ -59,11 +65,18 @@ def calculate_data(self): } storage_configs.append(storage_config) - self.data["apps"].append(appData) - self.data["storages"] = storage_configs + apps_data.append(appData) - for name, value in self.managedComponents.items(): - self.data[name] = value + self.data = { + "isVnet": self.wrapper_data.is_vnet(), + "certs": certs, + "apps": apps_data, + "storages": 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(), + } def get_template_name(self): return "main.bicep" diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 3aa4fc674e3..265bc729e23 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -2,9 +2,15 @@ # Concrete Converter Subclass for paramter class ParamConverter(ConverterTemplate): + + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) + def load_source(self, source): self.apps = source['apps'] - self.is_vnet = source['isVnet'] self.storages = source['storages'] def calculate_data(self): @@ -12,11 +18,11 @@ def calculate_data(self): storage['name'].split('/')[-1]: storage['properties']['accountName'] for storage in self.storages } - self.data.setdefault("apps", []) storage_configs = [] + apps_data = [] for app in self.apps: appName = app['name'].split('/')[-1] - self.data["apps"].append({ + apps_data.append({ "appName": appName, "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), "targetPort": "targetPortFor_"+appName.replace("-", "_"), @@ -41,8 +47,11 @@ def calculate_data(self): 'accountName': account_name, } storage_configs.append(storage_config) - self.data["storages"] = storage_configs - self.data["isVnet"] = self.is_vnet + self.data = { + "apps": apps_data, + "storages": storage_configs, + "isVnet": self.wrapper_data.is_vnet() + } 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 index 57c7ca28459..9ab64efe31a 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -2,6 +2,13 @@ # Concrete Converter Subclass for Read Me class ReadMeConverter(ConverterTemplate): + + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) + def load_source(self, source): pass diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index 9d0f38f9893..e515d95c82f 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -2,8 +2,11 @@ class ServiceRegistryConverter(ConverterTemplate): - def __init__(self): - super().__init__() + def __init__(self, input): + def extract_data(input): + # TODO: Implement the extract_data method + return input + super().__init__(input, extract_data) def load_source(self, source): self.source = source diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index cd4387797e1..5a14e693c73 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -27,19 +27,19 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): asa_arm = export_asa_arm_template(cmd, resource_group, service) # Create context and add converters - context = ConversionContext() - context.add_converter(MainConverter()) - context.add_converter(EnvironmentConverter()) - context.add_converter(AppConverter()) - context.add_converter(GatewayConverter(client, resource_group, service)) - context.add_converter(EurekaConverter()) - context.add_converter(ServiceRegistryConverter()) - context.add_converter(ConfigServerConverter()) - context.add_converter(ACSConverter()) - context.add_converter(LiveViewConverter()) - context.add_converter(ReadMeConverter()) - context.add_converter(ParamConverter()) - context.add_converter(CertConverter()) + context = ConversionContext(asa_arm) + context.add_converter(MainConverter(asa_arm)) + context.add_converter(EnvironmentConverter(asa_arm)) + context.add_converter(AppConverter(asa_arm)) + context.add_converter(GatewayConverter(asa_arm, client, resource_group, service)) + context.add_converter(EurekaConverter(asa_arm)) + context.add_converter(ServiceRegistryConverter(asa_arm)) + context.add_converter(ConfigServerConverter(asa_arm)) + context.add_converter(ACSConverter(asa_arm)) + context.add_converter(LiveViewConverter(asa_arm)) + context.add_converter(ReadMeConverter(asa_arm)) + context.add_converter(ParamConverter(asa_arm)) + context.add_converter(CertConverter(asa_arm)) # Prepare bicep parameters main_bicep_params = get_aca_bicep_params(asa_arm) From aa03ba4e08d91b30c19018506601d4ffdf23f87e Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 03:48:14 +0800 Subject: [PATCH 45/98] refactor env --- .../migration/converter/conversion_context.py | 2 +- .../converter/environment_converter.py | 64 +++++++++---------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 793b8e28fa9..e72ee5b9949 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -60,7 +60,7 @@ def run_converters(self, source): converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) elif cert['properties'].get('type') == "ContentCertificate": converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) - converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert(asa_service) + converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert() # Managed components Converter managed_components = { 'gateway': False, diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index ac8ed77fb6b..4bc2c26b7b5 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -5,45 +5,39 @@ class EnvironmentConverter(ConverterTemplate): def __init__(self, input): def extract_data(input): - # TODO: Implement the extract_data method - return input - super().__init__(input, extract_data) - - def load_source(self, source): - self.source = source - - def calculate_data(self): - name = self.source['name'].split('/')[-1] - apps = self.source.get('apps') - storages = self.source.get('storages') - self.data = { - "containerAppEnvName": name, - "containerAppLogAnalyticsName": f"log-{name}", - "identity": { - "type": self._get_identity_type(apps), - "userAssignedIdentities": self._get_user_assigned_identity_list(apps), - }, - "storages": self._get_app_storage_configs(apps, storages), - } - - if self.wrapper_data.is_vnet(): - self.data["vnetConfiguration"] = { - "internal": str(True).lower(), + asa_service = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + name = asa_service['name'].split('/')[-1] + apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + data = { + "containerAppEnvName": name, + "containerAppLogAnalyticsName": f"log-{name}", + "identity": { + "type": self._get_identity_type(apps), + "userAssignedIdentities": self._get_user_assigned_identity_list(apps), + }, + "storages": self._get_app_storage_configs(apps, storages), } - asa_zone_redundant = self.source['properties'].get('zoneRedundant') - if asa_zone_redundant is not None: - self.data["zoneRedundant"] = str(asa_zone_redundant).lower() + if self.wrapper_data.is_vnet(): + data["vnetConfiguration"] = { + "internal": str(True).lower(), + } - asa_maintenance_window = self.source['properties'].get('maintenanceScheduleConfiguration') - if asa_maintenance_window: - aca_maintenance_window = [{ - "weekDay": asa_maintenance_window['day'], - "startHourUtc": asa_maintenance_window['hour'], - "durationHours": 8, - }] - self.data["scheduledEntries"] = aca_maintenance_window + asa_zone_redundant = asa_service['properties'].get('zoneRedundant') + if asa_zone_redundant is not None: + data["zoneRedundant"] = str(asa_zone_redundant).lower() + asa_maintenance_window = asa_service['properties'].get('maintenanceScheduleConfiguration') + 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__(input, extract_data) def get_template_name(self): return "environment.bicep" From 1a62e63bb926f73e42c289a80921b6fff7f225bb Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 03:48:35 +0800 Subject: [PATCH 46/98] refactor gateway --- .../migration/converter/conversion_context.py | 18 +----- .../migration/converter/gateway_converter.py | 55 +++++++++---------- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index e72ee5b9949..76b3a1e5fa2 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -68,7 +68,8 @@ def run_converters(self, source): 'eureka': False, 'sba': False, } - converted_contents = self._convert_gateway(source_wrapper, converted_contents, managed_components) + converted_contents[self.get_converter(GatewayConverter).get_template_name()] = self.get_converter(GatewayConverter).convert() + logger.info(f"converted_contents for gateway: {converted_contents[self.get_converter(GatewayConverter).get_template_name()]}") if self.data_wrapper.is_support_ssoconfigserver(): converted_contents[self.get_converter(ConfigServerConverter).get_template_name()] = self.get_converter(ConfigServerConverter).convert() @@ -107,21 +108,6 @@ def save_to_files(self, converted_contents, output_path): logger.info(f"Generating the file {output_filename}...") output_file.write(content) - def _convert_gateway(self, source_wrapper, converted_contents, managed_components): - for gateway in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways'): - managed_components['gateway'] = True - gateway_key = self.get_converter(GatewayConverter).get_template_name() - routes = [] - for gateway_route in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways/routeConfigs'): - routes.append(gateway_route) - gateway_source = { - "gateway": gateway, - "routes": routes, - } - converted_contents[gateway_key] = self.get_converter(GatewayConverter).convert(gateway_source) - logger.info(f"converted_contents for gateway: {converted_contents[gateway_key]}") - return converted_contents - def _convert_live_view(self, source_wrapper, converted_contents, managed_components): for live_view in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews'): managed_components['sba'] = True diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 8b67de4530c..6602965fa4e 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -1,47 +1,46 @@ from .base_converter import ConverterTemplate +from knack.log import get_logger + +logger = get_logger(__name__) class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" def __init__(self, input, client, resource_group, service): def extract_data(input): - # TODO: Implement the extract_data method - return input - super().__init__(input, extract_data) + 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) + gatewayName = f"gateway" + 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) + return { + "routes": routes, + "gatewayName": gatewayName, + "configurations": configurations, + "replicas": replicas, + "routes": routes, + } self.client = client self.resource_group = resource_group self.service = service + super().__init__(input, extract_data) - def load_source(self, source): - self.source = source - secret_envs_dict = self.client.gateways.list_env_secrets(self.resource_group, self.service, self.DEFAULT_NAME) - self.source['secretEnvs'] = secret_envs_dict - - def calculate_data(self): - gatewayName = f"gateway" - configurations = self._get_configurations(self.source) - replicas = 2 - if self.source.get('gateway', {}).get('sku', {}).get('capacity') is not None: - replicas = min(2, self.source['gateway']['sku']['capacity']) - routes = self._get_routes(self.source['routes']) - - self.data = { - "gatewayName": gatewayName, - "configurations": configurations, - "replicas": replicas, - "routes": routes, - } - - def _get_configurations(self, source): + def _get_configurations(self, gateway, secretEnvs): configurations = [] - if source.get('gateway', {}).get('properties', {}).get('environmentVariables', {}).get('properties') is not None: - for key, value in source['gateway']['properties']['environmentVariables']['properties'].items(): + if gateway.get('properties', {}).get('environmentVariables', {}).get('properties') is not None: + for key, value in gateway['properties']['environmentVariables']['properties'].items(): configurations.append({ "propertyName": key, "value": value, }) - if source.get('secretEnvs') is not None: - for key, value in source['secretEnvs'].items(): + if secretEnvs is not None: + for key, value in secretEnvs.items(): configurations.append({ "propertyName": key, "value": value, From b3c8dfbc1bf5eee2a2426ae0316fb71ebdaf8280 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 03:59:59 +0800 Subject: [PATCH 47/98] refactor live-view --- .../migration/converter/conversion_context.py | 12 +++------ .../converter/live_view_converter.py | 25 +++++++------------ 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 76b3a1e5fa2..4be6dc0efb2 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -78,7 +78,9 @@ def run_converters(self, source): converted_contents[self.get_converter(ACSConverter).get_template_name()] = self.get_converter(ACSConverter).convert() logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[self.get_converter(ACSConverter).get_template_name()]}") - converted_contents = self._convert_live_view(source_wrapper, converted_contents, managed_components) + converted_contents[self.get_converter(LiveViewConverter).get_template_name()] = self.get_converter(LiveViewConverter).convert() + logger.info(f"converted_contents for Live View: {converted_contents[self.get_converter(LiveViewConverter).get_template_name()]}") + converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) converted_contents.update(self.get_converter(AppConverter).convert2()) @@ -108,14 +110,6 @@ def save_to_files(self, converted_contents, output_path): logger.info(f"Generating the file {output_filename}...") output_file.write(content) - def _convert_live_view(self, source_wrapper, converted_contents, managed_components): - for live_view in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews'): - managed_components['sba'] = True - live_view_key = self.get_converter(LiveViewConverter).get_template_name() - converted_contents[live_view_key] = self.get_converter(LiveViewConverter).convert(live_view) - logger.info(f"converted_contents for Live View: {converted_contents[live_view_key]}") - return converted_contents - def _convert_eureka_and_service_registry(self, source_wrapper, converted_contents, asa_service, managed_components): is_enterprise_tier = self.is_enterprise_tier(asa_service) for service_registry in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries'): diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 79b09360ed5..1136296e420 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -4,24 +4,17 @@ class LiveViewConverter(ConverterTemplate): def __init__(self, input): def extract_data(input): - # TODO: Implement the extract_data method - return input + 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 + } super().__init__(input, extract_data) - def load_source(self, source): - self.source = source - - def calculate_data(self): - name = "admin" - configurations = [] - replicas = 1 - - self.data = { - "sbaName": name, - "configurations": configurations, - "replicas": replicas - } - def get_template_name(self): return "spring_boot_admin.bicep" From 581664f4835ae71806f9079e8e2c35333dc6f3a4 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 04:22:12 +0800 Subject: [PATCH 48/98] refactor eureka --- .../migration/converter/base_converter.py | 5 +++- .../migration/converter/conversion_context.py | 23 ++++------------ .../converter/service_registry_converter.py | 26 +++++++------------ 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 9c73c290ffb..7ec323bb5bf 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -87,8 +87,11 @@ def is_support_acs(self): return self.is_support_feature('Microsoft.AppPlatform/Spring/configurationServices') def is_support_eureka(self): - return self.is_support_feature('Microsoft.AppPlatform/Spring/serviceRegistries') + 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_sba(self): return self.is_support_feature('Microsoft.AppPlatform/Spring/applicationLiveViews') diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 4be6dc0efb2..1685131aa6d 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -81,7 +81,11 @@ def run_converters(self, source): converted_contents[self.get_converter(LiveViewConverter).get_template_name()] = self.get_converter(LiveViewConverter).convert() logger.info(f"converted_contents for Live View: {converted_contents[self.get_converter(LiveViewConverter).get_template_name()]}") - converted_contents = self._convert_eureka_and_service_registry(source_wrapper, converted_contents, asa_service, managed_components) + converted_contents[self.get_converter(ServiceRegistryConverter).get_template_name()] = self.get_converter(ServiceRegistryConverter).convert() + logger.info(f"converted_contents for Service Registry: {converted_contents[self.get_converter(ServiceRegistryConverter).get_template_name()]}") + if not self.data_wrapper.is_enterprise_tier(): + converted_contents[self.get_converter(EurekaConverter).get_template_name()] = self.get_converter(EurekaConverter).convert() + logger.info(f"converted_contents for Eureka: {converted_contents[self.get_converter(EurekaConverter).get_template_name()]}") converted_contents.update(self.get_converter(AppConverter).convert2()) @@ -110,22 +114,5 @@ def save_to_files(self, converted_contents, output_path): logger.info(f"Generating the file {output_filename}...") output_file.write(content) - def _convert_eureka_and_service_registry(self, source_wrapper, converted_contents, asa_service, managed_components): - is_enterprise_tier = self.is_enterprise_tier(asa_service) - for service_registry in source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries'): - managed_components['eureka'] = True - eureka_key = self.get_converter(ServiceRegistryConverter).get_template_name() - converted_contents[eureka_key] = self.get_converter(ServiceRegistryConverter).convert(service_registry) - logger.info(f"converted_contents for Service Registry: {converted_contents[eureka_key]}") - return converted_contents - - if not is_enterprise_tier: - managed_components['eureka'] = True - eureka_key = self.get_converter(EurekaConverter).get_template_name() - converted_contents[eureka_key] = self.get_converter(EurekaConverter).convert(None) - return converted_contents - - def is_enterprise_tier(self, asa_service): - return asa_service['sku']['tier'] == 'Enterprise' diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index e515d95c82f..97999a6cb49 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -4,23 +4,17 @@ class ServiceRegistryConverter(ConverterTemplate): def __init__(self, input): def extract_data(input): - # TODO: Implement the extract_data method - return input - super().__init__(input, extract_data) - - def load_source(self, source): - self.source = source + service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') + name = f"eureka" + configurations = [] + replicas = 1 - def calculate_data(self): - name = f"eureka" - configurations = [] - replicas = 1 - - self.data = { - "eurekaName": name, - "configurations": configurations, - "replicas": replicas - } + return { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + super().__init__(input, extract_data) def get_template_name(self): return "eureka.bicep" From 44b00adb4d731bf708a648437cdc8c5c16852150 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 04:30:35 +0800 Subject: [PATCH 49/98] refactor param --- .../migration/converter/conversion_context.py | 11 +-- .../migration/converter/main_converter.py | 1 - .../migration/converter/param_converter.py | 86 +++++++++---------- 3 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 1685131aa6d..7ff76d9c2b6 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -61,13 +61,7 @@ def run_converters(self, source): elif cert['properties'].get('type') == "ContentCertificate": converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert() - # Managed components Converter - managed_components = { - 'gateway': False, - 'config': False, - 'eureka': False, - 'sba': False, - } + converted_contents[self.get_converter(GatewayConverter).get_template_name()] = self.get_converter(GatewayConverter).convert() logger.info(f"converted_contents for gateway: {converted_contents[self.get_converter(GatewayConverter).get_template_name()]}") @@ -94,11 +88,10 @@ def run_converters(self, source): "asa": asa_service, "apps": asa_apps, "certs": asa_kv_certs, - "managedComponents": managed_components, "storages": storages, } - converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert(full_source) + converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert() converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(full_source) converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert(full_source) diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 019ba928f7e..9b557fcf3ba 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -12,7 +12,6 @@ def extract_data(input): def load_source(self, source): self.source = source self.apps = source["apps"] - self.managedComponents = source["managedComponents"] self.certs = source["certs"] self.storages = source["storages"] diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 265bc729e23..72fe49076ae 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -5,53 +5,47 @@ class ParamConverter(ConverterTemplate): def __init__(self, input): def extract_data(input): - # TODO: Implement the extract_data method - return input + self.apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + self.storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in self.storages + } + storage_configs = [] + apps_data = [] + for app in self.apps: + appName = app['name'].split('/')[-1] + apps_data.append({ + "appName": appName, + "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "targetPort": "targetPortFor_"+appName.replace("-", "_"), + }) + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] + for disk_props in disks: + # Get the account name from storage map using storageId + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + app_name = app['name'].split('/')[-1] + account_name = storage_map.get(storage_name, '') + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + # print("storage_unique_name:", storage_unique_name) + storage_config = { + 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + 'accountName': account_name, + } + storage_configs.append(storage_config) + return { + "apps": apps_data, + "storages": storage_configs, + "isVnet": self.wrapper_data.is_vnet() + } super().__init__(input, extract_data) - def load_source(self, source): - self.apps = source['apps'] - self.storages = source['storages'] - - def calculate_data(self): - storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] - for storage in self.storages - } - storage_configs = [] - apps_data = [] - for app in self.apps: - appName = app['name'].split('/')[-1] - apps_data.append({ - "appName": appName, - "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "targetPort": "targetPortFor_"+appName.replace("-", "_"), - }) - if 'properties' in app and 'customPersistentDisks' in app['properties']: - disks = app['properties']['customPersistentDisks'] - for disk_props in disks: - # Get the account name from storage map using storageId - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' - app_name = app['name'].split('/')[-1] - account_name = storage_map.get(storage_name, '') - share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name - # print("storage_unique_name:", storage_unique_name) - storage_config = { - 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, - 'accountName': account_name, - } - storage_configs.append(storage_config) - self.data = { - "apps": apps_data, - "storages": storage_configs, - "isVnet": self.wrapper_data.is_vnet() - } - def get_template_name(self): return "param.bicepparam" From a14cd100432ec234665f834c7eadfda0c28c6625 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 04:48:59 +0800 Subject: [PATCH 50/98] refactor main and readme --- .../migration/converter/conversion_context.py | 20 +-- .../migration/converter/main_converter.py | 134 +++++++++--------- .../migration/converter/readme_converter.py | 6 - 3 files changed, 68 insertions(+), 92 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 7ff76d9c2b6..ea2a95af2b9 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -41,14 +41,6 @@ def set_params_for_converter(self, converter_type, params): def run_converters(self, source): converted_contents = {} source_wrapper = SourceDataWrapper(source) - asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] - asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - storages = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') - - # Environment Converter - asa_service['apps'] = asa_apps - asa_service['storages'] = storages - # Cert Converter asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') @@ -83,17 +75,9 @@ def run_converters(self, source): converted_contents.update(self.get_converter(AppConverter).convert2()) - # Param, readme and main Converter - full_source = { - "asa": asa_service, - "apps": asa_apps, - "certs": asa_kv_certs, - "storages": storages, - } - converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert() - converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert(full_source) - converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert(full_source) + converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert() + converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert() return converted_contents diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 9b557fcf3ba..8fd293b891d 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -5,77 +5,75 @@ class MainConverter(ConverterTemplate): def __init__(self, input): def extract_data(input): - # TODO: Implement the extract_data method - return input - super().__init__(input, extract_data) - - def load_source(self, source): - self.source = source - self.apps = source["apps"] - self.certs = source["certs"] - self.storages = source["storages"] - - def calculate_data(self): - certs = [] - for item in self.certs: - certName = item['name'].split('/')[-1] - moduleName = "cert_" + certName.replace("-", "_") - templateName = f"{certName}_cert.bicep" - certData = { - "certName": certName, - "moduleName": moduleName, - "templateName": templateName, + apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') + storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') + asa_kv_certs = [] + for cert in asa_certs: + certName = cert['name'].split('/')[-1] + if cert['properties'].get('type') == "KeyVaultCertificate": + asa_kv_certs.append(cert) + certs = [] + for item in asa_kv_certs: + certName = item['name'].split('/')[-1] + moduleName = "cert_" + certName.replace("-", "_") + templateName = f"{certName}_cert.bicep" + certData = { + "certName": certName, + "moduleName": moduleName, + "templateName": templateName, + } + certs.append(certData) + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in storages } - certs.append(certData) - storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] - for storage in self.storages - } - storage_configs = [] - apps_data = [] - for app in self.apps: - appName = app['name'].split('/')[-1] - moduleName = appName.replace("-", "_") - templateName = f"{appName}_app.bicep" - appData = { - "appName": appName, - "moduleName": moduleName, - "templateName": templateName, - "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "targetPort": "targetPortFor_"+appName.replace("-", "_"), - } - if 'properties' in app and 'customPersistentDisks' in app['properties']: - disks = app['properties']['customPersistentDisks'] - for disk_props in disks: - # Get the account name from storage map using storageId - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' - app_name = app['name'].split('/')[-1] - account_name = storage_map.get(storage_name, '') - share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) - # print("storage_unique_name:", storage_unique_name) - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name - storage_config = { - 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, - } - storage_configs.append(storage_config) + storage_configs = [] + apps_data = [] + for app in apps: + appName = app['name'].split('/')[-1] + moduleName = appName.replace("-", "_") + templateName = f"{appName}_app.bicep" + appData = { + "appName": appName, + "moduleName": moduleName, + "templateName": templateName, + "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "targetPort": "targetPortFor_"+appName.replace("-", "_"), + } + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties']['customPersistentDisks'] + for disk_props in disks: + # Get the account name from storage map using storageId + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + app_name = app['name'].split('/')[-1] + account_name = storage_map.get(storage_name, '') + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + # print("storage_unique_name:", storage_unique_name) + containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + storage_config = { + 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + } + storage_configs.append(storage_config) - apps_data.append(appData) + apps_data.append(appData) - self.data = { - "isVnet": self.wrapper_data.is_vnet(), - "certs": certs, - "apps": apps_data, - "storages": 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(), - } + return { + "isVnet": self.wrapper_data.is_vnet(), + "certs": certs, + "apps": apps_data, + "storages": 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__(input, extract_data) def get_template_name(self): return "main.bicep" diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 9ab64efe31a..a82b2d47dd4 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -9,11 +9,5 @@ def extract_data(input): return input super().__init__(input, extract_data) - def load_source(self, source): - pass - - def calculate_data(self): - pass - def get_template_name(self): return "readme.md" \ No newline at end of file From bcf386ca46ca836e6f8a1c2746fb1b4e5fcf2795 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 05:16:21 +0800 Subject: [PATCH 51/98] refactor cert --- .../migration/converter/cert_converter.py | 48 +++++++++++-------- .../migration/converter/conversion_context.py | 28 ++++------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 0a3e3d6d9dd..1213cc3e5bb 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -7,44 +7,52 @@ class CertConverter(ConverterTemplate): def __init__(self, input): def extract_data(input): - # TODO: Implement the extract_data method - return input + certs = [] + asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') + for cert in asa_certs: + if cert['properties'].get('type') == "KeyVaultCertificate": + certs.append(cert) + elif cert['properties'].get('type') == "ContentCertificate": + certs.append(cert) + return certs super().__init__(input, extract_data) - - def load_source(self, source): - self.source = source - # print(f"Cert source: {self.source}") - def calculate_data(self): - certName = self.source['name'].split('/')[-1] + def convert2(self): + outputs = {} + for cert in self.data: + certName = cert['name'].split('/')[-1] + certData = self.transform_data(cert) + outputs[certName+"_"+self.get_template_name()] = self.generate_output(certData) + return outputs + + def transform_data(self, cert): + certName = cert['name'].split('/')[-1] moduleName = "cert_" + certName.replace("-", "_") isKeyVaultCert = False - certKeyVault = self._get_cert_key_vault() - - self.data = { + certKeyVault = self._get_cert_key_vault(cert) + cert = { "certName": certName, "moduleName": moduleName, "certificateType": "ServerSSLCertificate", } - if certKeyVault: - self.data["certificateKeyVaultProperties"] = certKeyVault + cert["certificateKeyVaultProperties"] = certKeyVault isKeyVaultCert = True else: - self.data["value"] = "*" + cert["value"] = "*" isKeyVaultCert = False - self.data["isKeyVaultCert"] = isKeyVaultCert - # print(f"cert data: {self.data}") + cert["isKeyVaultCert"] = isKeyVaultCert + return cert def get_template_name(self): return "cert.bicep" - def _get_cert_key_vault(self): + def _get_cert_key_vault(self, cert): certKeyVault = None - if self.source['properties'].get('type') == "KeyVaultCertificate": - if self.source['properties'].get('vaultUri') and self.source['properties'].get('keyVaultCertName'): + if cert['properties'].get('type') == "KeyVaultCertificate": + if cert['properties'].get('vaultUri') and cert['properties'].get('keyVaultCertName'): certKeyVault = { - "keyVaultUrl": self.source['properties']['vaultUri'] + "/secrets/" + self.source['properties']['keyVaultCertName'], + "keyVaultUrl": cert['properties']['vaultUri'] + "/secrets/" + cert['properties']['keyVaultCertName'], "identity": "system" } return certKeyVault diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index ea2a95af2b9..be9ca166fb5 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -40,45 +40,37 @@ def set_params_for_converter(self, converter_type, params): def run_converters(self, source): converted_contents = {} - source_wrapper = SourceDataWrapper(source) - # Cert Converter - asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') - asa_kv_certs = [] - for cert in asa_certs: - certName = cert['name'].split('/')[-1] - if cert['properties'].get('type') == "KeyVaultCertificate": - asa_kv_certs.append(cert) - converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) - elif cert['properties'].get('type') == "ContentCertificate": - converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) + converted_contents.update(self.get_converter(CertConverter).convert2()) + # Environment Converter converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert() - + # Gateway Converter converted_contents[self.get_converter(GatewayConverter).get_template_name()] = self.get_converter(GatewayConverter).convert() logger.info(f"converted_contents for gateway: {converted_contents[self.get_converter(GatewayConverter).get_template_name()]}") - + # Config Server and ACS Converter if self.data_wrapper.is_support_ssoconfigserver(): converted_contents[self.get_converter(ConfigServerConverter).get_template_name()] = self.get_converter(ConfigServerConverter).convert() logger.debug(f"converted_contents for config server: {converted_contents[self.get_converter(ConfigServerConverter).get_template_name()]}") elif self.data_wrapper.is_support_acs(): converted_contents[self.get_converter(ACSConverter).get_template_name()] = self.get_converter(ACSConverter).convert() logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[self.get_converter(ACSConverter).get_template_name()]}") - + # Live View Converter converted_contents[self.get_converter(LiveViewConverter).get_template_name()] = self.get_converter(LiveViewConverter).convert() logger.info(f"converted_contents for Live View: {converted_contents[self.get_converter(LiveViewConverter).get_template_name()]}") - + # Service Registry and Eureka Converter converted_contents[self.get_converter(ServiceRegistryConverter).get_template_name()] = self.get_converter(ServiceRegistryConverter).convert() logger.info(f"converted_contents for Service Registry: {converted_contents[self.get_converter(ServiceRegistryConverter).get_template_name()]}") if not self.data_wrapper.is_enterprise_tier(): converted_contents[self.get_converter(EurekaConverter).get_template_name()] = self.get_converter(EurekaConverter).convert() logger.info(f"converted_contents for Eureka: {converted_contents[self.get_converter(EurekaConverter).get_template_name()]}") - + # App Converter converted_contents.update(self.get_converter(AppConverter).convert2()) - + # Param Converter converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert() + # ReadMe Converter converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert() + # Main Converter converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert() - return converted_contents def save_to_files(self, converted_contents, output_path): From d16e0837ba8d4cbe137350d8512890df57b47777 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 05:23:02 +0800 Subject: [PATCH 52/98] refactor extract_data method --- src/spring/azext_spring/migration/converter/acs_converter.py | 2 +- src/spring/azext_spring/migration/converter/app_converter.py | 2 +- src/spring/azext_spring/migration/converter/base_converter.py | 2 +- src/spring/azext_spring/migration/converter/cert_converter.py | 2 +- .../migration/converter/config_server_converter.py | 2 +- .../azext_spring/migration/converter/environment_converter.py | 2 +- .../azext_spring/migration/converter/eureka_converter.py | 4 ++-- .../azext_spring/migration/converter/gateway_converter.py | 2 +- .../azext_spring/migration/converter/live_view_converter.py | 2 +- src/spring/azext_spring/migration/converter/main_converter.py | 2 +- .../azext_spring/migration/converter/param_converter.py | 2 +- .../azext_spring/migration/converter/readme_converter.py | 4 ++-- .../migration/converter/service_registry_converter.py | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 2036be4c6a9..6bb0fced309 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -15,7 +15,7 @@ class ACSConverter(ConverterTemplate): KEY_PATTERN = ".pattern" def __init__(self, input): - def extract_data(input): + def extract_data(): acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0] name = f"config" configurations, params = self._get_configurations_and_params(acs) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 013b830da5b..6726d1c8805 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -12,7 +12,7 @@ class AppConverter(ConverterTemplate): DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" def __init__(self, input): - def extract_data(input): + def extract_data(): return self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') super().__init__(input, extract_data) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 7ec323bb5bf..451d74281c9 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -18,7 +18,7 @@ def __init__(self, input, extract_data): self.source = {} # input data of the converter self.wrapper_data = SourceDataWrapper(input) - self.data = extract_data(input=None) + self.data = extract_data() def set_params(self, params): self.params = params diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 1213cc3e5bb..e8a6630dc9b 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -6,7 +6,7 @@ class CertConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): certs = [] asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') for cert in asa_certs: diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index 361e7620e10..dd824c1d5aa 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -17,7 +17,7 @@ class ConfigServerConverter(ConverterTemplate): KEY_PATTERN = ".pattern" def __init__(self, input): - def extract_data(input): + def extract_data(): configServer = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers')[0] name = f"config" configurations, params = self._get_configurations_and_params(configServer) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 4bc2c26b7b5..71763409fef 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -4,7 +4,7 @@ class EnvironmentConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): asa_service = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] name = asa_service['name'].split('/')[-1] apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index adc4ad0b278..7376f556f28 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -3,9 +3,9 @@ class EurekaConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): # TODO: Implement the extract_data method - return input + pass super().__init__(input, extract_data) def load_source(self, source): diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 6602965fa4e..c823c9fd60b 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -7,7 +7,7 @@ class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" def __init__(self, input, client, resource_group, service): - def extract_data(input): + def extract_data(): 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'): diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 1136296e420..52ba3bbc98a 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -3,7 +3,7 @@ class LiveViewConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): live_view = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews')[0] name = "admin" configurations = [] diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 8fd293b891d..33ca5019941 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -4,7 +4,7 @@ class MainConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 72fe49076ae..df48a681f8f 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -4,7 +4,7 @@ class ParamConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): self.apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') self.storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') storage_map = { diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index a82b2d47dd4..d8bf4d7d1e7 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -4,9 +4,9 @@ class ReadMeConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): # TODO: Implement the extract_data method - return input + pass super().__init__(input, extract_data) def get_template_name(self): diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index 97999a6cb49..ca34fe0e05f 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -3,7 +3,7 @@ class ServiceRegistryConverter(ConverterTemplate): def __init__(self, input): - def extract_data(input): + def extract_data(): service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') name = f"eureka" configurations = [] From 14db9ba3d6c8074a6637a8be06ee6596d971d7bd Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 05:29:27 +0800 Subject: [PATCH 53/98] remove useless params field --- .../azext_spring/migration/converter/base_converter.py | 6 +----- .../migration/converter/conversion_context.py | 5 ----- .../azext_spring/migration/migration_operations.py | 10 ---------- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 451d74281c9..fdbc1f3839c 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -13,16 +13,12 @@ # 3. Generate the output data class ConverterTemplate(ABC): def __init__(self, input, extract_data): - self.params = {} # custom facing parameters for the converter self.data = {} # output data of the converter self.source = {} # input data of the converter self.wrapper_data = SourceDataWrapper(input) self.data = extract_data() - def set_params(self, params): - self.params = params - def convert(self, source=None): self.load_source(source) self.calculate_data() @@ -45,7 +41,7 @@ def generate_output(self, data): template_name = self.get_template_name() with open(f"{script_dir}/templates/{template_name}.j2") as file: template = Template(file.read()) - return template.render(data=data, params=self.params) + return template.render(data=data) # Extracts the resource name from a resource ID string in Azure ARM template format # Format: [resourceId('Microsoft.AppPlatform/Spring/', '', '')] diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index be9ca166fb5..d9d1d6ad5d3 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -33,11 +33,6 @@ def get_converter(self, converter_type: type): return converter raise ValueError(f"Unknown converter type: {converter_type}") - def set_params_for_converter(self, converter_type, params): - for converter in self.converters: - if isinstance(converter, converter_type): - converter.set_params(params) - def run_converters(self, source): converted_contents = {} # Cert Converter diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 5a14e693c73..9e96297d389 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -41,12 +41,6 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): context.add_converter(ParamConverter(asa_arm)) context.add_converter(CertConverter(asa_arm)) - # Prepare bicep parameters - main_bicep_params = get_aca_bicep_params(asa_arm) - - # Set parameters for EnvironmentConverter such as workload profile - context.set_params_for_converter(EnvironmentConverter, main_bicep_params) - # Run all converters logger.warning("Converting resources to Azure Container Apps...") converted_contents = context.run_converters(asa_arm) @@ -79,7 +73,3 @@ def export_asa_arm_template(cmd, resource_group, service): result = rcf.resource_groups.begin_export_template(resource_group, parameters=export_template_request) return result.template - - -def get_aca_bicep_params(asa_arm): - return {"key1": "value1"} From 216f59fb8a7d4e241efbe77785260580b9136528 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 05:37:27 +0800 Subject: [PATCH 54/98] refactor eureka --- .../migration/converter/eureka_converter.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 7376f556f28..900329325e0 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -4,23 +4,16 @@ class EurekaConverter(ConverterTemplate): def __init__(self, input): def extract_data(): - # TODO: Implement the extract_data method - pass - super().__init__(input, extract_data) - - def load_source(self, source): - self.source = source + name = "eureka" + configurations = [] + replicas = 1 - def calculate_data(self): - name = "eureka" - configurations = [] - replicas = 1 - - self.data = { - "eurekaName": name, - "configurations": configurations, - "replicas": replicas - } + return { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + super().__init__(input, extract_data) def get_template_name(self): return "eureka.bicep" From fd970c0f932ee1f06015aa1a669c085f9d62dab5 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 05:37:54 +0800 Subject: [PATCH 55/98] remove useless fields --- .../migration/converter/base_converter.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index fdbc1f3839c..1d6a0ec3c8c 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -7,31 +7,14 @@ # 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 input data into the output data -# The conversion process is divided into three steps: -# 1. Load the input data -# 2. Calculate the output data -# 3. Generate the output data class ConverterTemplate(ABC): def __init__(self, input, extract_data): - self.data = {} # output data of the converter - self.source = {} # input data of the converter - self.wrapper_data = SourceDataWrapper(input) self.data = extract_data() - def convert(self, source=None): - self.load_source(source) - self.calculate_data() + def convert(self): return self.generate_output(self.data) - # @abstractmethod # TODO: To be removed - def load_source(self, source): # load the input data - pass - - # @abstractmethod # TODO: To be removed - def calculate_data(self): # calculate the output data - pass - @abstractmethod def get_template_name(self): pass From 9213955b637546415b545eb42155d07b6aa59b24 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 05:56:58 +0800 Subject: [PATCH 56/98] refactor --- .../azext_spring/migration/converter/app_converter.py | 11 ----------- .../migration/converter/base_converter.py | 8 ++++++++ .../migration/converter/cert_converter.py | 10 ---------- .../migration/converter/conversion_context.py | 4 ++-- .../azext_spring/migration/migration_operations.py | 2 -- 5 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 6726d1c8805..c808ba72b58 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -1,8 +1,5 @@ -import os -import re from knack.log import get_logger from .base_converter import ConverterTemplate -from jinja2 import Template logger = get_logger(__name__) @@ -16,14 +13,6 @@ def extract_data(): return self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') super().__init__(input, extract_data) - def convert2(self): - outputs = {} - for app in self.data: - appName = app['name'].split('/')[-1] - appData = self.transform_data(app) - outputs[appName+"_"+self.get_template_name()] = self.generate_output(appData) - return outputs - def transform_data(self, app): appName = app['name'].split('/')[-1] envName = app['name'].split('/')[0] diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 1d6a0ec3c8c..a63ecb90d4d 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -15,6 +15,14 @@ def __init__(self, input, extract_data): def convert(self): return self.generate_output(self.data) + def convert_many(self): + outputs = {} + for item in self.data: + name = item['name'].split('/')[-1] + data = self.transform_data(item) + outputs[name+"_"+self.get_template_name()] = self.generate_output(data) + return outputs + @abstractmethod def get_template_name(self): pass diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index e8a6630dc9b..8443e377b7c 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -1,5 +1,3 @@ -import re - from .base_converter import ConverterTemplate # Concrete Converter Subclass for certificate @@ -17,14 +15,6 @@ def extract_data(): return certs super().__init__(input, extract_data) - def convert2(self): - outputs = {} - for cert in self.data: - certName = cert['name'].split('/')[-1] - certData = self.transform_data(cert) - outputs[certName+"_"+self.get_template_name()] = self.generate_output(certData) - return outputs - def transform_data(self, cert): certName = cert['name'].split('/')[-1] moduleName = "cert_" + certName.replace("-", "_") diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index d9d1d6ad5d3..6fb77eeaf70 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -36,7 +36,7 @@ def get_converter(self, converter_type: type): def run_converters(self, source): converted_contents = {} # Cert Converter - converted_contents.update(self.get_converter(CertConverter).convert2()) + converted_contents.update(self.get_converter(CertConverter).convert_many()) # Environment Converter converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert() # Gateway Converter @@ -59,7 +59,7 @@ def run_converters(self, source): converted_contents[self.get_converter(EurekaConverter).get_template_name()] = self.get_converter(EurekaConverter).convert() logger.info(f"converted_contents for Eureka: {converted_contents[self.get_converter(EurekaConverter).get_template_name()]}") # App Converter - converted_contents.update(self.get_converter(AppConverter).convert2()) + converted_contents.update(self.get_converter(AppConverter).convert_many()) # Param Converter converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert() # ReadMe Converter diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 9e96297d389..5bea044260a 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -1,5 +1,3 @@ -import os - 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 f75b9b7476d53a26e68837075a3508d4c17e1574 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 6 Mar 2025 10:18:19 +0800 Subject: [PATCH 57/98] command change --- src/spring/azext_spring/_help.py | 13 ++---- src/spring/azext_spring/_params.py | 3 +- src/spring/azext_spring/commands.py | 4 +- .../migration/converter/cert_converter.py | 4 -- .../migration/converter/conversion_context.py | 5 ++- .../converter/environment_converter.py | 40 +++++-------------- .../migration/converter/gateway_converter.py | 19 ++++++++- .../migration/converter/readme_converter.py | 2 +- .../converter/templates/environment.bicep.j2 | 10 +---- .../converter/templates/main.bicep.j2 | 18 --------- .../converter/templates/readme.md.j2 | 26 +++++------- .../migration/migration_operations.py | 6 +-- src/spring/azext_spring/spring_instance.py | 7 +++- 13 files changed, 59 insertions(+), 98 deletions(-) diff --git a/src/spring/azext_spring/_help.py b/src/spring/azext_spring/_help.py index d68428b994f..b68e8f444e1 100644 --- a/src/spring/azext_spring/_help.py +++ b/src/spring/azext_spring/_help.py @@ -1810,15 +1810,10 @@ text: az spring private-dns-zone clean --service MyAzureSpringAppsInstance --resource-group MyResourceGroup """ -helps['spring migration-aca'] = """ - type: group - short-summary: Commands to migrate from Azure Spring Apps to Azure Container Apps. -""" - -helps['spring migration-aca start'] = """ +helps['spring export'] = """ type: command - short-summary: Commands to start migration from Azure Spring Apps to Azure Container Apps. + 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 migration-aca start --service MyAzureSpringAppsInstance --resource-group MyResourceGroup + - 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 fca57db24f2..bdd6f44865c 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -1308,6 +1308,7 @@ def prepare_common_logs_argument(c): with self.argument_context('spring private-dns-zone clean') as c: c.argument('service', service_name_type) - with self.argument_context('spring migration-aca start') as c: + with self.argument_context('spring export') as c: c.argument('service', service_name_type) + c.argument('target', help='The target Azure service to migrate to, allowed values: aca, azure-container-apps.') 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 2d947f7601c..64f59834c1f 100644 --- a/src/spring/azext_spring/commands.py +++ b/src/spring/azext_spring/commands.py @@ -520,9 +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 migration-aca', custom_command_type=spring_routing_util, resource_type=ResourceType.MGMT_RESOURCE_RESOURCES, + 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('start', 'spring_migration_aca_start') + 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/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index bb856b7be09..9a096847f80 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -1,12 +1,9 @@ -import re - from .base_converter import ConverterTemplate # Concrete Converter Subclass for certificate class CertConverter(ConverterTemplate): def load_source(self, source): self.source = source - # print(f"Cert source: {self.source}") def calculate_data(self): certName = self.source['name'].split('/')[-1] @@ -27,7 +24,6 @@ def calculate_data(self): self.data["value"] = "*" isKeyVaultCert = False self.data["isKeyVaultCert"] = isKeyVaultCert - # print(f"cert data: {self.data}") def get_template_name(self): return "cert.bicep" diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 0d238cbfbfb..9cbd4b604d3 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -43,6 +43,7 @@ def run_converters(self, source): asa_service = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] asa_apps = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') storages = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') # Environment Converter is_vnet = self._is_vnet(asa_service) @@ -50,10 +51,8 @@ def run_converters(self, source): asa_service['isVnet'] = is_vnet asa_service['apps'] = asa_apps asa_service['storages'] = storages - # Cert Converter - asa_certs = source_wrapper.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') asa_kv_certs = [] for cert in asa_certs: certName = cert['name'].split('/')[-1] @@ -62,6 +61,8 @@ def run_converters(self, source): converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) elif cert['properties'].get('type') == "ContentCertificate": converted_contents[certName+"_"+self.get_converter(CertConverter).get_template_name()] = self.get_converter(CertConverter).convert(cert) + + asa_service['certs'] = asa_kv_certs converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert(asa_service) # Managed components Converter managed_components = { diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index efa4f33cc36..526802580a1 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -8,16 +8,17 @@ def load_source(self, source): def calculate_data(self): name = self.source['name'].split('/')[-1] apps = self.source.get('apps') + certs = self.source.get('certs') storages = self.source.get('storages') self.data = { "containerAppEnvName": name, "containerAppLogAnalyticsName": f"log-{name}", - "identity": { - "type": self._get_identity_type(apps), - "userAssignedIdentities": self._get_user_assigned_identity_list(apps), - }, "storages": self._get_app_storage_configs(apps, storages), } + if self._need_identity(certs): + self.data["identity"] = { + "type": "SystemAssigned", + } isVnet = self.source['isVnet'] if isVnet: @@ -42,33 +43,10 @@ def calculate_data(self): def get_template_name(self): return "environment.bicep" - def _get_identity_type(self, apps): - type = None - hasUserAssignedIdentity = False - hasSystemAssignedIdentity = False - for app in apps: - if app.get('identity') is not None: - if 'SystemAssigned' in app['identity'].get('type'): - hasSystemAssignedIdentity = True - elif 'UserAssigned' in app['identity'].get('type'): - hasUserAssignedIdentity = True - if hasUserAssignedIdentity and hasSystemAssignedIdentity: - type = "SystemAssigned,UserAssigned" - elif hasUserAssignedIdentity: - type = "UserAssigned" - elif hasSystemAssignedIdentity: - type = "SystemAssigned" - return type - - def _get_user_assigned_identity_list(self, apps): - user_assigned_identities = [] - for app in apps: - if app.get('identity') is not None: - if 'UserAssigned' in app['identity'].get('type'): - if app.get('identity').get('userAssignedIdentities') is not None: - for id in app['identity']['userAssignedIdentities']: - user_assigned_identities.append(id) - return user_assigned_identities + def _need_identity(self, certs): + if certs is not None and len(certs) > 0: + return True + return False def _get_app_storage_configs(self, apps, storages): storage_configs = [] diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index b7e0bdbc6d7..ec76296f851 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -1,5 +1,7 @@ +from knack.log import get_logger from .base_converter import ConverterTemplate +logger = get_logger(__name__) class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" @@ -21,6 +23,7 @@ def calculate_data(self): if self.source.get('gateway', {}).get('sku', {}).get('capacity') is not None: replicas = min(2, self.source['gateway']['sku']['capacity']) routes = self._get_routes(self.source['routes']) + self._check_features(self.source.get('gateway', {}).get('properties', {})) self.data = { "gatewayName": gatewayName, @@ -60,7 +63,7 @@ def _get_routes(self, routes): "id": f"{base_name}_{count}", "uri": r.get('uri', aca_uri), "predicates": r.get('predicates') if r.get('predicates') else [], - "filters": r.get('filters') if r.get('filters') else [], + "filters": self._get_filters(base_name, r), "order": r.get('order') or 0, }) return aca_routes @@ -77,6 +80,20 @@ def _get_app_name_from_app_resource_id(self, app_resource_id): 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"Mismatch: 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.") + 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: + logger.warning("CORS configuration detected, please refer to public doc to migrate CORS feature of gateway for Spring to Azure Container Apps.") def get_template_name(self): return "gateway.bicep" diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 57c7ca28459..39581fd213a 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -9,4 +9,4 @@ def calculate_data(self): pass def get_template_name(self): - return "readme.md" \ No newline at end of file + return "README.md" \ 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 index dd4e3cab612..8fb3133a6de 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -34,17 +34,9 @@ resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { name: '{{data.containerAppEnvName}}' location: resourceGroup().location - {%- if data.identity.type %} + {%- 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: { diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 51ca387e3d3..1636a22bfe1 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -34,9 +34,6 @@ module containerAppEnv 'environment.bicep' = { {%- for item in data.certs %} module {{ item.moduleName }} '{{ item.templateName }}' = { name: 'cert-{{ item.certName }}-Deployment' - dependsOn: [ - containerAppEnv - ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -46,9 +43,6 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {%- for item in data.apps %} module {{ item.moduleName }} '{{ item.templateName }}' = { name: '{{ item.appName }}-Deployment' - dependsOn: [ - containerAppEnv - ] params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId workloadProfileName: workloadProfileName @@ -61,9 +55,6 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {%- if data.gateway == true %} module managedGateway 'gateway.bicep' = { name: 'gateway-Deployment' - dependsOn: [ - containerAppEnv - ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -73,9 +64,6 @@ module managedGateway 'gateway.bicep' = { {%- if data.config == true %} module managedConfig 'config_server.bicep' = { name: 'config-server-Deployment' - dependsOn: [ - containerAppEnv - ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -85,9 +73,6 @@ module managedConfig 'config_server.bicep' = { {%- if data.eureka == true %} module managedEureka 'eureka.bicep' = { name: 'eureka-Deployment' - dependsOn: [ - containerAppEnv - ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } @@ -97,9 +82,6 @@ module managedEureka 'eureka.bicep' = { {%- if data.sba == true %} module managedSpringBootAdmin 'spring_boot_admin.bicep' = { name: 'spring-boot-admin-Deployment' - dependsOn: [ - containerAppEnv - ] params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index 3c868fceb9b..d6aafcb085f 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -6,16 +6,10 @@ After deploying the Bicep files, some configurations must be updated manually by Before getting started, make sure you have the following: -1. **Azure Subscription**: An Azure subscription with sufficient permissions to create and manage resources for deploying Azure Container Apps. +1. **Azure Subscription**: An Azure subscription for resources of Azure Container Apps. 2. **Bicep CLI**: Verify that the Bicep CLI is installed. -3. **Generated Bicep files**: Ensure you have run the following command to generate the Bicep files from your Azure Spring Apps service to Azure Container Apps. - -```azurecli -az spring migration-aca start --service --resource-group --subscription --output-folder -``` - ## Steps to Deploy Azure Container Apps Using the Bicep Files ### Check Output Message @@ -28,7 +22,7 @@ During the script execution, two types of messages may appear: "Mismatch Detected: Property 'X' in Azure Spring Apps does not match any Property in Azure Container Apps." ``` -2. **Action Message**: Occasionally, some port 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: +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." @@ -72,15 +66,17 @@ During the migration process, you may encounter warnings or errors caused by pro **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 +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 +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: ``` @@ -99,17 +95,17 @@ You can check the deployment status either in the CLI output or in the Azure Por - 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... | Assign Access Control List permissions to system assigned managed identity.​ | - | JavaComponentOperationError | Failed to create config map external-auth-config-map for JavaComponent '' in k8se-system namespace. | Redeploy the bicep files.​ | + |---|---|---| + | 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. | ## Post-Deployment Configuration Some properties and configurations are not included in the Bicep files and must be manually updated. -- **Custom Domain**: If you have a custom domain configured in Azure Spring Apps, the TLS certificates from Azure Key Vault have been migrated by the migration tool, but you need to follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. -- **Managed Identity**: If you have enabled a system-assigned managed identity in Azure Spring Apps, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal%2Cdotnet) to understand how to use managed identity in Azure Container Apps. +- **Custom Domain**: If you have a custom domain configured in Azure Spring Apps, the 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. Then you can follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. +- **Managed Identity**: If you have enabled a system-assigned managed identity in Azure Spring Apps applications, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. - **Blue-green deployment**: If you 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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. - **Auto scale**: If you have enabled auto scaling in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index cd4387797e1..c72f7873e78 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -22,7 +22,7 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): - logger.warning("Getting your Azure Spring Apps service...") + 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) @@ -48,13 +48,13 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): context.set_params_for_converter(EnvironmentConverter, main_bicep_params) # Run all converters - logger.warning("Converting resources to Azure Container Apps...") + logger.info("Converting resources to Azure Container Apps...") converted_contents = context.run_converters(asa_arm) 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.") + 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): diff --git a/src/spring/azext_spring/spring_instance.py b/src/spring/azext_spring/spring_instance.py index 0e709ede604..171b0afcf92 100644 --- a/src/spring/azext_spring/spring_instance.py +++ b/src/spring/azext_spring/spring_instance.py @@ -327,5 +327,8 @@ def spring_private_dns_zone_clean(cmd, client, resource_group, service): resource_group_name=resource_group, service_name=service, resource=updated_resource) -def spring_migration_aca_start(cmd, client, resource_group, service, output_folder=None): - migration_aca_start(cmd, client, resource_group, service, output_folder) +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'.") From 94691e50fa663be211916212196a8e47fdf0bbfa Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 10:48:20 +0800 Subject: [PATCH 58/98] rename param to avoid new concept --- .../migration/converter/acs_converter.py | 4 ++-- .../migration/converter/app_converter.py | 12 ++++++------ .../migration/converter/base_converter.py | 6 +++--- .../migration/converter/cert_converter.py | 4 ++-- .../migration/converter/config_server_converter.py | 4 ++-- .../migration/converter/conversion_context.py | 4 ++-- .../migration/converter/environment_converter.py | 4 ++-- .../migration/converter/eureka_converter.py | 4 ++-- .../migration/converter/gateway_converter.py | 4 ++-- .../migration/converter/live_view_converter.py | 4 ++-- .../migration/converter/main_converter.py | 4 ++-- .../migration/converter/param_converter.py | 4 ++-- .../migration/converter/readme_converter.py | 4 ++-- .../converter/service_registry_converter.py | 4 ++-- 14 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 6bb0fced309..fccf1123c99 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -14,7 +14,7 @@ class ACSConverter(ConverterTemplate): KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm" KEY_PATTERN = ".pattern" - def __init__(self, input): + def __init__(self, source): def extract_data(): acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0] name = f"config" @@ -26,7 +26,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(input, extract_data) + super().__init__(source, extract_data) def get_template_name(self): return "config_server.bicep" diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index c808ba72b58..c0559c6453b 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -8,10 +8,10 @@ class AppConverter(ConverterTemplate): DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" - def __init__(self, input): + def __init__(self, source): def extract_data(): return self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - super().__init__(input, extract_data) + super().__init__(source, extract_data) def transform_data(self, app): appName = app['name'].split('/')[-1] @@ -89,9 +89,9 @@ def get_template_name(self): def get_app_name(input_string): return input_string.split('/')[-1] - def _get_service_bind(self, source, envName): + def _get_service_bind(self, app, envName): service_bind = [] - addon = source['properties'].get('addonConfigs') + addon = app['properties'].get('addonConfigs') if addon is None: return None @@ -259,8 +259,8 @@ def _convert_http_probe_action(self, probe, tier): probeAction = None return probeAction - def _get_ingress(self, source, tier): - ingress = source['properties'].get('ingressSettings') + def _get_ingress(self, app, tier): + ingress = app['properties'].get('ingressSettings') if ingress is None: return None return { diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index a63ecb90d4d..cfb013a429b 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -6,10 +6,10 @@ # 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 input data into the output data +# The responsibility of the converter is to convert the source data into the output data class ConverterTemplate(ABC): - def __init__(self, input, extract_data): - self.wrapper_data = SourceDataWrapper(input) + def __init__(self, source, extract_data): + self.wrapper_data = SourceDataWrapper(source) self.data = extract_data() def convert(self): diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 8443e377b7c..53148f29d7a 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -3,7 +3,7 @@ # Concrete Converter Subclass for certificate class CertConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): certs = [] asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') @@ -13,7 +13,7 @@ def extract_data(): elif cert['properties'].get('type') == "ContentCertificate": certs.append(cert) return certs - super().__init__(input, extract_data) + super().__init__(source, extract_data) def transform_data(self, cert): certName = cert['name'].split('/')[-1] diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index dd824c1d5aa..a98e87bc0f9 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -16,7 +16,7 @@ class ConfigServerConverter(ConverterTemplate): KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm" KEY_PATTERN = ".pattern" - def __init__(self, input): + def __init__(self, source): def extract_data(): configServer = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers')[0] name = f"config" @@ -28,7 +28,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(input, extract_data) + super().__init__(source, extract_data) def get_template_name(self): return "config_server.bicep" diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 6fb77eeaf70..76c53346e22 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -20,8 +20,8 @@ # Context Class class ConversionContext: - def __init__(self, input): - self.data_wrapper = SourceDataWrapper(input) + def __init__(self, source): + self.data_wrapper = SourceDataWrapper(source) self.converters = [] def add_converter(self, converter: ConverterTemplate): diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 71763409fef..00754d52eb2 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -3,7 +3,7 @@ # Concrete Subclass for Container App Environment class EnvironmentConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): asa_service = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] name = asa_service['name'].split('/')[-1] @@ -37,7 +37,7 @@ def extract_data(): }] data["scheduledEntries"] = aca_maintenance_window return data - super().__init__(input, extract_data) + super().__init__(source, extract_data) def get_template_name(self): return "environment.bicep" diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 900329325e0..75039e83bed 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -2,7 +2,7 @@ class EurekaConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): name = "eureka" configurations = [] @@ -13,7 +13,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(input, extract_data) + super().__init__(source, extract_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 index c823c9fd60b..07e18d13c7f 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -6,7 +6,7 @@ class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" - def __init__(self, input, client, resource_group, service): + def __init__(self, source, client, resource_group, service): def extract_data(): gateway = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/gateways')[0] routes = [] @@ -29,7 +29,7 @@ def extract_data(): self.client = client self.resource_group = resource_group self.service = service - super().__init__(input, extract_data) + super().__init__(source, extract_data) def _get_configurations(self, gateway, secretEnvs): configurations = [] diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 52ba3bbc98a..b92db939a68 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -2,7 +2,7 @@ class LiveViewConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): live_view = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews')[0] name = "admin" @@ -13,7 +13,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(input, extract_data) + super().__init__(source, extract_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 index 33ca5019941..5823c9eb14c 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -3,7 +3,7 @@ # Concrete Converter Subclass for Read Me class MainConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') @@ -73,7 +73,7 @@ def extract_data(): "eureka": self.wrapper_data.is_support_eureka(), "sba": self.wrapper_data.is_support_sba(), } - super().__init__(input, extract_data) + super().__init__(source, extract_data) def get_template_name(self): return "main.bicep" diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index df48a681f8f..106c366fc23 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -3,7 +3,7 @@ # Concrete Converter Subclass for paramter class ParamConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): self.apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') self.storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') @@ -45,7 +45,7 @@ def extract_data(): "storages": storage_configs, "isVnet": self.wrapper_data.is_vnet() } - super().__init__(input, extract_data) + super().__init__(source, extract_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 index d8bf4d7d1e7..f4383e366fc 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -3,11 +3,11 @@ # Concrete Converter Subclass for Read Me class ReadMeConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): # TODO: Implement the extract_data method pass - super().__init__(input, extract_data) + super().__init__(source, extract_data) def get_template_name(self): return "readme.md" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index ca34fe0e05f..2f8e9dd1a25 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -2,7 +2,7 @@ class ServiceRegistryConverter(ConverterTemplate): - def __init__(self, input): + def __init__(self, source): def extract_data(): service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') name = f"eureka" @@ -14,7 +14,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(input, extract_data) + super().__init__(source, extract_data) def get_template_name(self): return "eureka.bicep" From 437c6400a668975052a245b8546824abc9806d17 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 10:57:17 +0800 Subject: [PATCH 59/98] refactor run_converters --- .../migration/converter/base_converter.py | 4 +- .../migration/converter/conversion_context.py | 63 +++++++++++++------ .../migration/migration_operations.py | 2 +- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index cfb013a429b..d53bbe85ef5 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -13,7 +13,9 @@ def __init__(self, source, extract_data): self.data = extract_data() def convert(self): - return self.generate_output(self.data) + outputs = {} + outputs[self.get_template_name()] =self.generate_output(self.data) + return outputs def convert_many(self): outputs = {} diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 76c53346e22..ff7607c5ecc 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -33,39 +33,62 @@ def get_converter(self, converter_type: type): return converter raise ValueError(f"Unknown converter type: {converter_type}") - def run_converters(self, source): + def run_converters(self): converted_contents = {} # Cert Converter - converted_contents.update(self.get_converter(CertConverter).convert_many()) + certs = self.get_converter(CertConverter).convert_many() + converted_contents.update(certs) + for cert in certs.keys(): + logger.debug(f"converted_contents for cert {cert}:\n{converted_contents.get(cert)}") + # Environment Converter - converted_contents[self.get_converter(EnvironmentConverter).get_template_name()] = self.get_converter(EnvironmentConverter).convert() + converted_contents.update(self.get_converter(EnvironmentConverter).convert()) + logger.debug(f"converted_contents for environment:\n{converted_contents.get(self.get_converter(EnvironmentConverter).get_template_name())}") + # Gateway Converter - converted_contents[self.get_converter(GatewayConverter).get_template_name()] = self.get_converter(GatewayConverter).convert() - logger.info(f"converted_contents for gateway: {converted_contents[self.get_converter(GatewayConverter).get_template_name()]}") + if self.data_wrapper.is_support_gateway(): + converted_contents.update(self.get_converter(GatewayConverter).convert()) + logger.debug(f"converted_contents for gateway:\n{converted_contents.get(self.get_converter(GatewayConverter).get_template_name())}") + # Config Server and ACS Converter if self.data_wrapper.is_support_ssoconfigserver(): - converted_contents[self.get_converter(ConfigServerConverter).get_template_name()] = self.get_converter(ConfigServerConverter).convert() - logger.debug(f"converted_contents for config server: {converted_contents[self.get_converter(ConfigServerConverter).get_template_name()]}") + converted_contents.update(self.get_converter(ConfigServerConverter).convert()) + logger.debug(f"converted_contents for config server:\n{converted_contents.get(self.get_converter(ConfigServerConverter).get_template_name())}") elif self.data_wrapper.is_support_acs(): - converted_contents[self.get_converter(ACSConverter).get_template_name()] = self.get_converter(ACSConverter).convert() - logger.debug(f"converted_contents for Application Configuration Service: {converted_contents[self.get_converter(ACSConverter).get_template_name()]}") + converted_contents.update(self.get_converter(ACSConverter).convert()) + logger.debug(f"converted_contents for Application Configuration Service:\n{converted_contents.get(self.get_converter(ACSConverter).get_template_name())}") + # Live View Converter - converted_contents[self.get_converter(LiveViewConverter).get_template_name()] = self.get_converter(LiveViewConverter).convert() - logger.info(f"converted_contents for Live View: {converted_contents[self.get_converter(LiveViewConverter).get_template_name()]}") + if self.data_wrapper.is_support_sba(): + converted_contents.update(self.get_converter(LiveViewConverter).convert()) + logger.debug(f"converted_contents for Live View:\n{converted_contents.get(self.get_converter(LiveViewConverter).get_template_name())}") + # Service Registry and Eureka Converter - converted_contents[self.get_converter(ServiceRegistryConverter).get_template_name()] = self.get_converter(ServiceRegistryConverter).convert() - logger.info(f"converted_contents for Service Registry: {converted_contents[self.get_converter(ServiceRegistryConverter).get_template_name()]}") - if not self.data_wrapper.is_enterprise_tier(): - converted_contents[self.get_converter(EurekaConverter).get_template_name()] = self.get_converter(EurekaConverter).convert() - logger.info(f"converted_contents for Eureka: {converted_contents[self.get_converter(EurekaConverter).get_template_name()]}") + if self.data_wrapper.is_enterprise_tier(): + if self.data_wrapper.is_support_service_registry(): + converted_contents.update(self.get_converter(ServiceRegistryConverter).convert()) + logger.debug(f"converted_contents for Service Registry:\n{converted_contents.get(self.get_converter(ServiceRegistryConverter).get_template_name())}") + else: # Basic Tier or Standard Tier + converted_contents.update(self.get_converter(EurekaConverter).convert()) + logger.debug(f"converted_contents for Eureka:\n{converted_contents.get(self.get_converter(EurekaConverter).get_template_name())}") + # App Converter - converted_contents.update(self.get_converter(AppConverter).convert_many()) + apps = self.get_converter(AppConverter).convert_many() + converted_contents.update(apps) + for app in apps.keys(): + logger.debug(f"converted_contents for App {app}:\n{converted_contents.get(app)}") + # Param Converter - converted_contents[self.get_converter(ParamConverter).get_template_name()] = self.get_converter(ParamConverter).convert() + converted_contents.update(self.get_converter(ParamConverter).convert()) + logger.debug(f"converted_contents for Param:\n{converted_contents.get(self.get_converter(ParamConverter).get_template_name())}") + # ReadMe Converter - converted_contents[self.get_converter(ReadMeConverter).get_template_name()] = self.get_converter(ReadMeConverter).convert() + converted_contents.update(self.get_converter(ReadMeConverter).convert()) + logger.debug(f"converted_contents for ReadMe:\n{converted_contents.get(self.get_converter(ReadMeConverter).get_template_name())}") + # Main Converter - converted_contents[self.get_converter(MainConverter).get_template_name()] = self.get_converter(MainConverter).convert() + converted_contents.update(self.get_converter(MainConverter).convert()) + logger.debug(f"converted_contents for Main:\n{converted_contents.get(self.get_converter(MainConverter).get_template_name())}") return converted_contents def save_to_files(self, converted_contents, output_path): diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 5bea044260a..1cdd18c1208 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -41,7 +41,7 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): # Run all converters logger.warning("Converting resources to Azure Container Apps...") - converted_contents = context.run_converters(asa_arm) + 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 From c0b9ffefc894032080550e1072a506d89994537e Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Thu, 6 Mar 2025 16:05:30 +0800 Subject: [PATCH 60/98] refactor more --- .../migration/converter/acs_converter.py | 4 +-- .../migration/converter/app_converter.py | 10 +++---- .../migration/converter/base_converter.py | 30 ++++++++++++++++--- .../migration/converter/cert_converter.py | 21 +++++++------ .../converter/config_server_converter.py | 4 +-- .../migration/converter/conversion_context.py | 30 +++++++++---------- .../converter/environment_converter.py | 10 +++---- .../migration/converter/eureka_converter.py | 4 +-- .../migration/converter/gateway_converter.py | 4 +-- .../converter/live_view_converter.py | 4 +-- .../migration/converter/main_converter.py | 17 ++++------- .../migration/converter/param_converter.py | 8 ++--- .../migration/converter/readme_converter.py | 6 ++-- .../converter/service_registry_converter.py | 4 +-- 14 files changed, 86 insertions(+), 70 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index fccf1123c99..964202a1ccc 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -15,7 +15,7 @@ class ACSConverter(ConverterTemplate): KEY_PATTERN = ".pattern" def __init__(self, source): - def extract_data(): + def transform_data(): acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0] name = f"config" configurations, params = self._get_configurations_and_params(acs) @@ -26,7 +26,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(source, extract_data) + super().__init__(source, transform_data) def get_template_name(self): return "config_server.bicep" diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index c0559c6453b..68712a9a1bc 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -9,11 +9,11 @@ class AppConverter(ConverterTemplate): DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" def __init__(self, source): - def extract_data(): - return self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - super().__init__(source, extract_data) + def transform_data(): + return self.wrapper_data.get_apps() + super().__init__(source, transform_data) - def transform_data(self, app): + def transform_data_item(self, app): appName = app['name'].split('/')[-1] envName = app['name'].split('/')[0] asa_deployments = self.wrapper_data.get_deployments_by_app(app['name']) @@ -26,7 +26,7 @@ def transform_data(self, app): ingress = self._get_ingress(app, tier) isPublic = app['properties'].get('public') identity = app.get('identity') - storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + storages = self.wrapper_data.get_storages() # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") volumeMounts = [] volumes = [] diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index d53bbe85ef5..6b59861dda7 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -1,16 +1,19 @@ 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, extract_data): + def __init__(self, source, transform_data): self.wrapper_data = SourceDataWrapper(source) - self.data = extract_data() + self.data = transform_data() def convert(self): outputs = {} @@ -21,7 +24,7 @@ def convert_many(self): outputs = {} for item in self.data: name = item['name'].split('/')[-1] - data = self.transform_data(item) + data = self.transform_data_item(item) outputs[name+"_"+self.get_template_name()] = self.generate_output(data) return outputs @@ -107,4 +110,23 @@ 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 \ No newline at end of file + 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') \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 53148f29d7a..9e620a048d6 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -1,21 +1,20 @@ +from knack.log import get_logger from .base_converter import ConverterTemplate +logger = get_logger(__name__) + # Concrete Converter Subclass for certificate class CertConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): - certs = [] - asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') - for cert in asa_certs: - if cert['properties'].get('type') == "KeyVaultCertificate": - certs.append(cert) - elif cert['properties'].get('type') == "ContentCertificate": - certs.append(cert) - return certs - super().__init__(source, extract_data) + def transform_data(): + asa_content_certs = self.wrapper_data.get_content_certificates() + for cert in asa_content_certs: + logger.warning(f"Mismatch: The content certificate: {cert['name']} cannot be exported automatically. Please export it manually.") + return self.wrapper_data.get_certificates() + super().__init__(source, transform_data) - def transform_data(self, cert): + def transform_data_item(self, cert): certName = cert['name'].split('/')[-1] moduleName = "cert_" + certName.replace("-", "_") isKeyVaultCert = False diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index a98e87bc0f9..03a758ef7a8 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -17,7 +17,7 @@ class ConfigServerConverter(ConverterTemplate): KEY_PATTERN = ".pattern" def __init__(self, source): - def extract_data(): + def transform_data(): configServer = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers')[0] name = f"config" configurations, params = self._get_configurations_and_params(configServer) @@ -28,7 +28,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(source, extract_data) + super().__init__(source, transform_data) def get_template_name(self): return "config_server.bicep" diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index ff7607c5ecc..24a02fbeeda 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -38,57 +38,57 @@ def run_converters(self): # Cert Converter certs = self.get_converter(CertConverter).convert_many() converted_contents.update(certs) - for cert in certs.keys(): - logger.debug(f"converted_contents for cert {cert}:\n{converted_contents.get(cert)}") + # for cert in certs.keys(): + # logger.debug(f"converted_contents for cert {cert}:\n{converted_contents.get(cert)}") # Environment Converter converted_contents.update(self.get_converter(EnvironmentConverter).convert()) - logger.debug(f"converted_contents for environment:\n{converted_contents.get(self.get_converter(EnvironmentConverter).get_template_name())}") + # logger.debug(f"converted_contents for environment:\n{converted_contents.get(self.get_converter(EnvironmentConverter).get_template_name())}") # Gateway Converter if self.data_wrapper.is_support_gateway(): converted_contents.update(self.get_converter(GatewayConverter).convert()) - logger.debug(f"converted_contents for gateway:\n{converted_contents.get(self.get_converter(GatewayConverter).get_template_name())}") + # logger.debug(f"converted_contents for gateway:\n{converted_contents.get(self.get_converter(GatewayConverter).get_template_name())}") # Config Server and ACS Converter if self.data_wrapper.is_support_ssoconfigserver(): converted_contents.update(self.get_converter(ConfigServerConverter).convert()) - logger.debug(f"converted_contents for config server:\n{converted_contents.get(self.get_converter(ConfigServerConverter).get_template_name())}") + # logger.debug(f"converted_contents for config server:\n{converted_contents.get(self.get_converter(ConfigServerConverter).get_template_name())}") elif self.data_wrapper.is_support_acs(): converted_contents.update(self.get_converter(ACSConverter).convert()) - logger.debug(f"converted_contents for Application Configuration Service:\n{converted_contents.get(self.get_converter(ACSConverter).get_template_name())}") + # logger.debug(f"converted_contents for Application Configuration Service:\n{converted_contents.get(self.get_converter(ACSConverter).get_template_name())}") # Live View Converter if self.data_wrapper.is_support_sba(): converted_contents.update(self.get_converter(LiveViewConverter).convert()) - logger.debug(f"converted_contents for Live View:\n{converted_contents.get(self.get_converter(LiveViewConverter).get_template_name())}") + # logger.debug(f"converted_contents for Live View:\n{converted_contents.get(self.get_converter(LiveViewConverter).get_template_name())}") # Service Registry and Eureka Converter if self.data_wrapper.is_enterprise_tier(): - if self.data_wrapper.is_support_service_registry(): + if self.data_wrapper.is_support_serviceregistry(): converted_contents.update(self.get_converter(ServiceRegistryConverter).convert()) - logger.debug(f"converted_contents for Service Registry:\n{converted_contents.get(self.get_converter(ServiceRegistryConverter).get_template_name())}") + # logger.debug(f"converted_contents for Service Registry:\n{converted_contents.get(self.get_converter(ServiceRegistryConverter).get_template_name())}") else: # Basic Tier or Standard Tier converted_contents.update(self.get_converter(EurekaConverter).convert()) - logger.debug(f"converted_contents for Eureka:\n{converted_contents.get(self.get_converter(EurekaConverter).get_template_name())}") + # logger.debug(f"converted_contents for Eureka:\n{converted_contents.get(self.get_converter(EurekaConverter).get_template_name())}") # App Converter apps = self.get_converter(AppConverter).convert_many() converted_contents.update(apps) - for app in apps.keys(): - logger.debug(f"converted_contents for App {app}:\n{converted_contents.get(app)}") + # for app in apps.keys(): + # logger.debug(f"converted_contents for App {app}:\n{converted_contents.get(app)}") # Param Converter converted_contents.update(self.get_converter(ParamConverter).convert()) - logger.debug(f"converted_contents for Param:\n{converted_contents.get(self.get_converter(ParamConverter).get_template_name())}") + # logger.debug(f"converted_contents for Param:\n{converted_contents.get(self.get_converter(ParamConverter).get_template_name())}") # ReadMe Converter converted_contents.update(self.get_converter(ReadMeConverter).convert()) - logger.debug(f"converted_contents for ReadMe:\n{converted_contents.get(self.get_converter(ReadMeConverter).get_template_name())}") + # logger.debug(f"converted_contents for ReadMe:\n{converted_contents.get(self.get_converter(ReadMeConverter).get_template_name())}") # Main Converter converted_contents.update(self.get_converter(MainConverter).convert()) - logger.debug(f"converted_contents for Main:\n{converted_contents.get(self.get_converter(MainConverter).get_template_name())}") + # logger.debug(f"converted_contents for Main:\n{converted_contents.get(self.get_converter(MainConverter).get_template_name())}") return converted_contents def save_to_files(self, converted_contents, output_path): diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 127486596c6..fb6b0bf6436 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -4,12 +4,12 @@ class EnvironmentConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): + def transform_data(): asa_service = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] name = asa_service['name'].split('/')[-1] - apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') - certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') + apps = self.wrapper_data.get_apps() + storages = self.wrapper_data.get_storages() + certs = self.wrapper_data.get_certificates() data = { "containerAppEnvName": name, "containerAppLogAnalyticsName": f"log-{name}", @@ -37,7 +37,7 @@ def extract_data(): }] data["scheduledEntries"] = aca_maintenance_window return data - super().__init__(source, extract_data) + super().__init__(source, transform_data) def get_template_name(self): return "environment.bicep" diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 75039e83bed..2ddccbb038c 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -3,7 +3,7 @@ class EurekaConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): + def transform_data(): name = "eureka" configurations = [] replicas = 1 @@ -13,7 +13,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(source, extract_data) + 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 index 5bc231570fe..4deeae95206 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -8,7 +8,7 @@ class GatewayConverter(ConverterTemplate): DEFAULT_NAME = "default" def __init__(self, source, client, resource_group, service): - def extract_data(): + def transform_data(): 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'): @@ -30,7 +30,7 @@ def extract_data(): self.client = client self.resource_group = resource_group self.service = service - super().__init__(source, extract_data) + super().__init__(source, transform_data) def _get_configurations(self, gateway, secretEnvs): configurations = [] diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index b92db939a68..2f210f04eb2 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -3,7 +3,7 @@ class LiveViewConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): + def transform_data(): live_view = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews')[0] name = "admin" configurations = [] @@ -13,7 +13,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(source, extract_data) + 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 index 5823c9eb14c..c367808d973 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -4,17 +4,12 @@ class MainConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): - apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') - asa_certs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/certificates') - asa_kv_certs = [] - for cert in asa_certs: - certName = cert['name'].split('/')[-1] - if cert['properties'].get('type') == "KeyVaultCertificate": - asa_kv_certs.append(cert) + def transform_data(): + apps = self.wrapper_data.get_apps() + storages = self.wrapper_data.get_storages() + asa_certs = self.wrapper_data.get_certificates() certs = [] - for item in asa_kv_certs: + for item in asa_certs: certName = item['name'].split('/')[-1] moduleName = "cert_" + certName.replace("-", "_") templateName = f"{certName}_cert.bicep" @@ -73,7 +68,7 @@ def extract_data(): "eureka": self.wrapper_data.is_support_eureka(), "sba": self.wrapper_data.is_support_sba(), } - super().__init__(source, extract_data) + super().__init__(source, transform_data) def get_template_name(self): return "main.bicep" diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 106c366fc23..af7b30aaa81 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -4,9 +4,9 @@ class ParamConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): - self.apps = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/apps') - self.storages = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') + def transform_data(): + self.apps = self.wrapper_data.get_apps() + self.storages = self.wrapper_data.get_storages() storage_map = { storage['name'].split('/')[-1]: storage['properties']['accountName'] for storage in self.storages @@ -45,7 +45,7 @@ def extract_data(): "storages": storage_configs, "isVnet": self.wrapper_data.is_vnet() } - super().__init__(source, extract_data) + 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 index 5895bc0c096..bce5afdffbf 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -4,10 +4,10 @@ class ReadMeConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): - # TODO: Implement the extract_data method + def transform_data(): + # TODO: Implement the transform_data method pass - super().__init__(source, extract_data) + super().__init__(source, transform_data) def get_template_name(self): return "README.md" \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index 2f8e9dd1a25..d6bfff450a3 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -3,7 +3,7 @@ class ServiceRegistryConverter(ConverterTemplate): def __init__(self, source): - def extract_data(): + def transform_data(): service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') name = f"eureka" configurations = [] @@ -14,7 +14,7 @@ def extract_data(): "configurations": configurations, "replicas": replicas } - super().__init__(source, extract_data) + super().__init__(source, transform_data) def get_template_name(self): return "eureka.bicep" From 9fddace2f55dfae0fb8774cccc20cdcb49f021a4 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 11:24:45 +0800 Subject: [PATCH 61/98] refactor _get_storage_unique_name method --- .../migration/converter/app_converter.py | 11 +---------- .../migration/converter/base_converter.py | 14 +++++++++++++- .../migration/converter/environment_converter.py | 5 ++--- .../migration/converter/main_converter.py | 15 +-------------- .../migration/converter/param_converter.py | 7 +------ 5 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 68712a9a1bc..3c7904be846 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -26,16 +26,11 @@ def transform_data_item(self, app): ingress = self._get_ingress(app, tier) isPublic = app['properties'].get('public') identity = app.get('identity') - storages = self.wrapper_data.get_storages() # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") volumeMounts = [] volumes = [] if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] - storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] - for storage in storages - } for disk_props in disks: # print(f"Disk props: {disk_props}") storage_id = disk_props.get('storageId', '') @@ -47,12 +42,8 @@ def transform_data_item(self, app): mountOptions = "" for option in disk_props.get('customPersistentDiskProperties').get('mountOptions'): mountOptions += ("," if mountOptions != "" else "") + option - account_name = storage_map.get(storage_name, '') mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' - storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + storage_unique_name = self._get_storage_unique_name(disk_props) # print("Mount options: ", mountOptions) volumeMounts.append({ "volumeName": storage_name, diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 6b59861dda7..7cf880ca7e3 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -53,7 +53,19 @@ def _get_resource_name(self, resource_id): # print(f"Resource name: {result}") return result - def _get_storage_unique_name(self, storage_name, account_name, share_name, mount_path, access_mode): + def _get_storage_unique_name(self, disk_props): + storages = self.wrapper_data.get_storages() + storage_map = { + storage['name'].split('/')[-1]: storage['properties']['accountName'] + for storage in storages + } + storage_id = disk_props.get('storageId', '') + storage_name = self._get_resource_name(storage_id) if storage_id else '' + account_name = storage_map.get(storage_name, '') + share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') + mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') + readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) + access_mode = 'ReadOnly' if readOnly else 'ReadWrite' 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}" diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index fb6b0bf6436..f629067bf43 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -5,7 +5,7 @@ class EnvironmentConverter(ConverterTemplate): def __init__(self, source): def transform_data(): - asa_service = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] + asa_service = self.wrapper_data.get_asa_service() name = asa_service['name'].split('/')[-1] apps = self.wrapper_data.get_apps() storages = self.wrapper_data.get_storages() @@ -69,9 +69,8 @@ def _get_app_storage_configs(self, apps, storages): app_name = app['name'].split('/')[-1] readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) access_mode = 'ReadOnly' if readOnly else 'ReadWrite' - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) - storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name # print("storage_unique_name:", storage_unique_name) diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index c367808d973..da575e38b9d 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -6,7 +6,6 @@ class MainConverter(ConverterTemplate): def __init__(self, source): def transform_data(): apps = self.wrapper_data.get_apps() - storages = self.wrapper_data.get_storages() asa_certs = self.wrapper_data.get_certificates() certs = [] for item in asa_certs: @@ -19,10 +18,6 @@ def transform_data(): "templateName": templateName, } certs.append(certData) - storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] - for storage in storages - } storage_configs = [] apps_data = [] for app in apps: @@ -40,15 +35,7 @@ def transform_data(): disks = app['properties']['customPersistentDisks'] for disk_props in disks: # Get the account name from storage map using storageId - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' - app_name = app['name'].split('/')[-1] - account_name = storage_map.get(storage_name, '') - share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + storage_unique_name = self._get_storage_unique_name(disk_props) # print("storage_unique_name:", storage_unique_name) containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name storage_config = { diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index af7b30aaa81..36d87842efe 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -26,13 +26,8 @@ def transform_data(): # Get the account name from storage map using storageId storage_id = disk_props.get('storageId', '') storage_name = self._get_resource_name(storage_id) if storage_id else '' - app_name = app['name'].split('/')[-1] account_name = storage_map.get(storage_name, '') - share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - storage_unique_name = self._get_storage_unique_name(storage_name, account_name, share_name, mount_path, access_mode) + storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name # print("storage_unique_name:", storage_unique_name) storage_config = { From 5d2e52724ecc01a4cb189c4de3da278fce89ed5e Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 11:37:36 +0800 Subject: [PATCH 62/98] extra method _get_storage_name --- .../azext_spring/migration/converter/app_converter.py | 5 +---- .../azext_spring/migration/converter/base_converter.py | 7 +++++-- .../migration/converter/environment_converter.py | 3 +-- .../azext_spring/migration/converter/param_converter.py | 3 +-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 3c7904be846..734c6f857cd 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -32,10 +32,7 @@ def transform_data_item(self, app): if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] for disk_props in disks: - # print(f"Disk props: {disk_props}") - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' - # print("Storage name: ", storage_name) + storage_name = self._get_storage_name(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: diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 7cf880ca7e3..6a85fd6899f 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -53,14 +53,17 @@ def _get_resource_name(self, resource_id): # print(f"Resource name: {result}") return result + def _get_storage_name(self, disk_props): + storage_id = disk_props.get('storageId', '') + return self._get_resource_name(storage_id) if storage_id else '' + def _get_storage_unique_name(self, disk_props): storages = self.wrapper_data.get_storages() storage_map = { storage['name'].split('/')[-1]: storage['properties']['accountName'] for storage in storages } - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' + storage_name = self._get_storage_name(disk_props) account_name = storage_map.get(storage_name, '') share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index f629067bf43..d3e6a71bf20 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -62,8 +62,7 @@ def _get_app_storage_configs(self, apps, storages): disks = app['properties']['customPersistentDisks'] for disk_props in disks: # Get the account name from storage map using storageId - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' + storage_name = self._get_storage_name(disk_props) account_name = storage_map.get(storage_name, '') share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') app_name = app['name'].split('/')[-1] diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 36d87842efe..3b2bd5267df 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -24,8 +24,7 @@ def transform_data(): disks = app['properties']['customPersistentDisks'] for disk_props in disks: # Get the account name from storage map using storageId - storage_id = disk_props.get('storageId', '') - storage_name = self._get_resource_name(storage_id) if storage_id else '' + storage_name = self._get_storage_name(disk_props) account_name = storage_map.get(storage_name, '') storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name From 0bdac0744c01520acd764b0d26557aa8be1aec6f Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 11:38:18 +0800 Subject: [PATCH 63/98] extra _get_account_name method --- .../migration/converter/base_converter.py | 8 ++++++-- .../migration/converter/environment_converter.py | 14 +++----------- .../migration/converter/param_converter.py | 8 +------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 6a85fd6899f..332c7edf3f7 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -57,14 +57,18 @@ def _get_storage_name(self, disk_props): storage_id = disk_props.get('storageId', '') return self._get_resource_name(storage_id) if storage_id else '' - def _get_storage_unique_name(self, disk_props): + def _get_account_name(self, disk_props): storages = self.wrapper_data.get_storages() storage_map = { storage['name'].split('/')[-1]: storage['properties']['accountName'] for storage in storages } storage_name = self._get_storage_name(disk_props) - account_name = storage_map.get(storage_name, '') + 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_account_name(disk_props) share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index d3e6a71bf20..935237b83fc 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -8,12 +8,11 @@ def transform_data(): asa_service = self.wrapper_data.get_asa_service() name = asa_service['name'].split('/')[-1] apps = self.wrapper_data.get_apps() - storages = self.wrapper_data.get_storages() certs = self.wrapper_data.get_certificates() data = { "containerAppEnvName": name, "containerAppLogAnalyticsName": f"log-{name}", - "storages": self._get_app_storage_configs(apps, storages), + "storages": self._get_app_storage_configs(apps), } if self._need_identity(certs): data["identity"] = { @@ -47,15 +46,8 @@ def _need_identity(self, certs): return True return False - def _get_app_storage_configs(self, apps, storages): + def _get_app_storage_configs(self, apps): storage_configs = [] - - # Create a mapping of storage IDs to account names - storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] - for storage in storages - } - # print("storage_map:", storage_map) for app in apps: # Check if app has properties and customPersistentDiskProperties if 'properties' in app and 'customPersistentDisks' in app['properties']: @@ -63,7 +55,7 @@ def _get_app_storage_configs(self, apps, storages): for disk_props in disks: # Get the account name from storage map using storageId storage_name = self._get_storage_name(disk_props) - account_name = storage_map.get(storage_name, '') + account_name = self._get_account_name(disk_props) share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') app_name = app['name'].split('/')[-1] readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 3b2bd5267df..ef9dc3710af 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -6,11 +6,6 @@ class ParamConverter(ConverterTemplate): def __init__(self, source): def transform_data(): self.apps = self.wrapper_data.get_apps() - self.storages = self.wrapper_data.get_storages() - storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] - for storage in self.storages - } storage_configs = [] apps_data = [] for app in self.apps: @@ -24,8 +19,7 @@ def transform_data(): disks = app['properties']['customPersistentDisks'] for disk_props in disks: # Get the account name from storage map using storageId - storage_name = self._get_storage_name(disk_props) - account_name = storage_map.get(storage_name, '') + account_name = self._get_account_name(disk_props) storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name # print("storage_unique_name:", storage_unique_name) From 74c9914a4742a3ab320e8460431f2c09360be060 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 12:25:57 +0800 Subject: [PATCH 64/98] extra get_mount_options method --- .../migration/converter/app_converter.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 734c6f857cd..553b1f98d46 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -33,15 +33,8 @@ def transform_data_item(self, app): disks = app['properties']['customPersistentDisks'] for disk_props in disks: storage_name = self._get_storage_name(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 mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') storage_unique_name = self._get_storage_unique_name(disk_props) - # print("Mount options: ", mountOptions) volumeMounts.append({ "volumeName": storage_name, "mountPath": mount_path, @@ -49,7 +42,7 @@ def transform_data_item(self, app): volumes.append({ "volumeName": storage_name, "storageName": storage_unique_name, - "mountOptions": mountOptions, + "mountOptions": self.get_mount_options(disk_props), }) # print("Volume mounts: ", volumeMounts) # print("Volumes: ", volumes) @@ -71,6 +64,16 @@ def transform_data_item(self, app): "volumes": volumes, } + 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_template_name(self): return "app.bicep" From 7895a58f54ade9cd02d301c421e5619e955f77f2 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 12:32:33 +0800 Subject: [PATCH 65/98] introduce BaseConverter class --- src/spring/azext_spring/migration/converter/acs_converter.py | 4 ++-- src/spring/azext_spring/migration/converter/app_converter.py | 4 ++-- src/spring/azext_spring/migration/converter/base_converter.py | 3 +++ src/spring/azext_spring/migration/converter/cert_converter.py | 4 ++-- .../migration/converter/config_server_converter.py | 4 ++-- .../azext_spring/migration/converter/environment_converter.py | 4 ++-- .../azext_spring/migration/converter/eureka_converter.py | 4 ++-- .../azext_spring/migration/converter/gateway_converter.py | 4 ++-- .../azext_spring/migration/converter/live_view_converter.py | 4 ++-- src/spring/azext_spring/migration/converter/main_converter.py | 4 ++-- .../azext_spring/migration/converter/param_converter.py | 4 ++-- .../azext_spring/migration/converter/readme_converter.py | 4 ++-- .../migration/converter/service_registry_converter.py | 4 ++-- 13 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 964202a1ccc..bccd72c8d1c 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -1,7 +1,7 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter # Concrete Converter Subclass for Config Server -class ACSConverter(ConverterTemplate): +class ACSConverter(BaseConverter): CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" KEY_URI = ".uri" diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 553b1f98d46..52b9fd32ed3 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -1,10 +1,10 @@ from knack.log import get_logger -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter logger = get_logger(__name__) # Concrete Converter Subclass for Container App -class AppConverter(ConverterTemplate): +class AppConverter(BaseConverter): DEFAULT_MOUNT_OPTIONS = "uid=0,gid=0,file_mode=0777,dir_mode=0777" diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 332c7edf3f7..7a39d11d59c 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -39,6 +39,9 @@ def generate_output(self, data): 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): # 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')] diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 9e620a048d6..790833eaf02 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -1,10 +1,10 @@ from knack.log import get_logger -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter logger = get_logger(__name__) # Concrete Converter Subclass for certificate -class CertConverter(ConverterTemplate): +class CertConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index 03a758ef7a8..22a9681e251 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -1,9 +1,9 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter from knack.log import get_logger logger = get_logger(__name__) # Concrete Converter Subclass for Config Server -class ConfigServerConverter(ConverterTemplate): +class ConfigServerConverter(BaseConverter): CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git" KEY_URI = ".uri" diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 935237b83fc..1442ace6eb8 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -1,7 +1,7 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter # Concrete Subclass for Container App Environment -class EnvironmentConverter(ConverterTemplate): +class EnvironmentConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 2ddccbb038c..467ef8fccd3 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -1,6 +1,6 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter -class EurekaConverter(ConverterTemplate): +class EurekaConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 4deeae95206..4c1af09905b 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -1,10 +1,10 @@ from knack.log import get_logger -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter from knack.log import get_logger logger = get_logger(__name__) -class GatewayConverter(ConverterTemplate): +class GatewayConverter(BaseConverter): DEFAULT_NAME = "default" def __init__(self, source, client, resource_group, service): diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 2f210f04eb2..8e52f2ee6bf 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -1,6 +1,6 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter -class LiveViewConverter(ConverterTemplate): +class LiveViewConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index da575e38b9d..a16f81ea48a 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -1,7 +1,7 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter # Concrete Converter Subclass for Read Me -class MainConverter(ConverterTemplate): +class MainConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index ef9dc3710af..ab327c9ec2b 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -1,7 +1,7 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter # Concrete Converter Subclass for paramter -class ParamConverter(ConverterTemplate): +class ParamConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index bce5afdffbf..b80c887d562 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -1,7 +1,7 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter # Concrete Converter Subclass for Read Me -class ReadMeConverter(ConverterTemplate): +class ReadMeConverter(BaseConverter): def __init__(self, source): def transform_data(): diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index d6bfff450a3..f0650dcbad5 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -1,6 +1,6 @@ -from .base_converter import ConverterTemplate +from .base_converter import BaseConverter -class ServiceRegistryConverter(ConverterTemplate): +class ServiceRegistryConverter(BaseConverter): def __init__(self, source): def transform_data(): From ae8e16ac7b989f518a5b8a14c21fba61446bb026 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 13:05:00 +0800 Subject: [PATCH 66/98] rename param variable start with "param" --- .../migration/converter/app_converter.py | 4 ++-- .../converter/environment_converter.py | 4 ++-- .../migration/converter/main_converter.py | 8 ++++---- .../migration/converter/param_converter.py | 8 ++++---- .../converter/templates/app.bicep.j2 | 20 +++++++++---------- .../converter/templates/environment.bicep.j2 | 4 ++-- .../converter/templates/main.bicep.j2 | 12 +++++------ .../converter/templates/param.bicepparam.j2 | 6 +++--- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 52b9fd32ed3..bc11897a1f8 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -48,8 +48,8 @@ def transform_data_item(self, app): # print("Volumes: ", volumes) return { "containerAppName": appName, - "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "targetPort": "targetPortFor_"+appName.replace("-", "_"), + "paramContainerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "paramTargetPort": "targetPortFor_"+appName.replace("-", "_"), "moduleName": moduleName, "ingress": ingress, "isPublic": isPublic, diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 1442ace6eb8..84feb6ad278 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -63,11 +63,11 @@ def _get_app_storage_configs(self, apps): # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + paramContainerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name # print("storage_unique_name:", storage_unique_name) storage_config = { 'containerAppEnvStorageName': containerAppEnvStorageName, - 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + 'paramContainerAppEnvStorageAccountKey': paramContainerAppEnvStorageAccountKey, 'storageName': storage_unique_name, 'shareName': share_name, 'accessMode': access_mode, diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index a16f81ea48a..c972ccd5f4d 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -28,8 +28,8 @@ def transform_data(): "appName": appName, "moduleName": moduleName, "templateName": templateName, - "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "targetPort": "targetPortFor_"+appName.replace("-", "_"), + "paramContainerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "paramTargetPort": "targetPortFor_"+appName.replace("-", "_"), } if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] @@ -37,9 +37,9 @@ def transform_data(): # Get the account name from storage map using storageId storage_unique_name = self._get_storage_unique_name(disk_props) # print("storage_unique_name:", storage_unique_name) - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + paramContainerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name storage_config = { - 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + 'paramContainerAppEnvStorageAccountKey': paramContainerAppEnvStorageAccountKey, } storage_configs.append(storage_config) diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index ab327c9ec2b..6682c23d32a 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -12,8 +12,8 @@ def transform_data(): appName = app['name'].split('/')[-1] apps_data.append({ "appName": appName, - "containerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "targetPort": "targetPortFor_"+appName.replace("-", "_"), + "paramContainerAppImageName": "containerImageFor_"+appName.replace("-", "_"), + "paramTargetPort": "targetPortFor_"+appName.replace("-", "_"), }) if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] @@ -21,10 +21,10 @@ def transform_data(): # Get the account name from storage map using storageId account_name = self._get_account_name(disk_props) storage_unique_name = self._get_storage_unique_name(disk_props) - containerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name + paramContainerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name # print("storage_unique_name:", storage_unique_name) storage_config = { - 'containerAppEnvStorageAccountKey': containerAppEnvStorageAccountKey, + 'paramContainerAppEnvStorageAccountKey': paramContainerAppEnvStorageAccountKey, 'accountName': account_name, } storage_configs.append(storage_config) diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index d50a0b21a47..86f5901d625 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -1,6 +1,6 @@ param containerAppName string = '{{data.containerAppName}}' -param {{data.containerAppImageName}} string -param {{data.targetPort}} int +param {{data.paramContainerAppImageName}} string +param {{data.paramTargetPort}} int @description('Minimum number of replicas that will be deployed') @minValue(0) @@ -36,7 +36,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { ingress: { {%- if data.ingress %} external: {% if data.isPublic %}true{% else %}false{% endif %} - targetPort: {{data.targetPort}} + targetPort: {{data.paramTargetPort}} allowInsecure: false transport: '{{data.ingress.transport}}' {%- if data.ingress.sessionAffinity %} @@ -60,7 +60,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { containers: [ { name: containerAppName - image: {{data.containerAppImageName}} + image: {{data.paramContainerAppImageName}} resources: { cpu: json('{{data.blue.cpuCore}}') memory: '{{data.blue.memorySize}}' @@ -82,13 +82,13 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { {%- if data.blue.livenessProbe.httpGet %} httpGet: { path: '{{data.blue.livenessProbe.httpGet.path}}' - port: {{data.targetPort}} + port: {{data.paramTargetPort}} scheme: '{{data.blue.livenessProbe.httpGet.scheme}}' } {%- endif %} {%- if data.blue.livenessProbe.tcpSocket %} tcpSocket: { - port: {{data.targetPort}} + port: {{data.paramTargetPort}} } {%- endif %} {%- if data.blue.livenessProbe.initialDelaySeconds %} @@ -114,13 +114,13 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { {%- if data.blue.readinessProbe.httpGet %} httpGet: { path: '{{data.blue.readinessProbe.httpGet.path}}' - port: {{data.targetPort}} + port: {{data.paramTargetPort}} scheme: '{{data.blue.readinessProbe.httpGet.scheme}}' } {%- endif %} {%- if data.blue.readinessProbe.tcpSocket %} tcpSocket: { - port: {{data.targetPort}} + port: {{data.paramTargetPort}} } {%- endif %} {%- if data.blue.readinessProbe.initialDelaySeconds %} @@ -146,13 +146,13 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { {%- if data.blue.startupProbe.httpGet %} httpGet: { path: '{{data.blue.startupProbe.httpGet.path}}' - port: {{data.targetPort}} + port: {{data.paramTargetPort}} scheme: '{{data.blue.startupProbe.httpGet.scheme}}' } {%- endif %} {%- if data.blue.startupProbe.tcpSocket %} tcpSocket: { - port: {{data.targetPort}} + port: {{data.paramTargetPort}} } {%- endif %} {%- if data.blue.startupProbe.initialDelaySeconds %} diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 8fb3133a6de..02d5afcf4e0 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -79,7 +79,7 @@ resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigu {%- if data.storages %} {%- for storage in data.storages %} -param {{storage.containerAppEnvStorageAccountKey}} string +param {{storage.paramContainerAppEnvStorageAccountKey}} string resource {{storage.containerAppEnvStorageName}} 'Microsoft.App/managedEnvironments/storages@2024-08-02-preview' = { parent: containerAppEnv name: '{{storage.storageName}}' @@ -88,7 +88,7 @@ resource {{storage.containerAppEnvStorageName}} 'Microsoft.App/managedEnvironmen accountName: '{{storage.accountName}}' shareName: '{{storage.shareName}}' accessMode: '{{storage.accessMode}}' - accountKey: {{storage.containerAppEnvStorageAccountKey}} + accountKey: {{storage.paramContainerAppEnvStorageAccountKey}} } } } diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 1636a22bfe1..4c3b5ae09c8 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -8,11 +8,11 @@ param maxNodes int param vnetSubnetId string {%- endif %} {%- for app in data.apps %} -param {{app.containerAppImageName}} string -param {{app.targetPort}} int +param {{app.paramContainerAppImageName}} string +param {{app.paramTargetPort}} int {%- endfor %} {%- for storage in data.storages %} -param {{storage.containerAppEnvStorageAccountKey}} string +param {{storage.paramContainerAppEnvStorageAccountKey}} string {%- endfor %} module containerAppEnv 'environment.bicep' = { @@ -26,7 +26,7 @@ module containerAppEnv 'environment.bicep' = { vnetSubnetId: vnetSubnetId {%- endif %} {%- for storage in data.storages %} - {{storage.containerAppEnvStorageAccountKey}}: {{storage.containerAppEnvStorageAccountKey}} + {{storage.paramContainerAppEnvStorageAccountKey}}: {{storage.paramContainerAppEnvStorageAccountKey}} {%- endfor %} } } @@ -46,8 +46,8 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId workloadProfileName: workloadProfileName - {{item.containerAppImageName}}: {{item.containerAppImageName}} - {{item.targetPort}}: {{item.targetPort}} + {{item.paramContainerAppImageName}}: {{item.paramContainerAppImageName}} + {{item.paramTargetPort}}: {{item.paramTargetPort}} } } {%- endfor %} diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index fa4142aa1ee..7354e52661e 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -12,10 +12,10 @@ param vnetSubnetId {%- endif %} {%- for app in data.apps %} -param {{app.containerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' -param {{app.targetPort}} = 80 +param {{app.paramContainerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' +param {{app.paramTargetPort}} = 80 {%- endfor %} {%- for storage in data.storages %} -param {{storage.containerAppEnvStorageAccountKey}} = 'fill in account key for storage "{{storage.accountName}}"' +param {{storage.paramContainerAppEnvStorageAccountKey}} = 'fill in account key for storage "{{storage.accountName}}"' {%- endfor %} \ No newline at end of file From 0b5b2983fac4b645aac8e4d9638189afdfcc0b8b Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 13:31:32 +0800 Subject: [PATCH 67/98] extra method for get param name --- .../migration/converter/app_converter.py | 4 ++-- .../migration/converter/base_converter.py | 15 +++++++++++++++ .../migration/converter/environment_converter.py | 3 +-- .../migration/converter/main_converter.py | 10 +++------- .../migration/converter/param_converter.py | 13 ++++--------- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index bc11897a1f8..bc8ebee0417 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -48,8 +48,8 @@ def transform_data_item(self, app): # print("Volumes: ", volumes) return { "containerAppName": appName, - "paramContainerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "paramTargetPort": "targetPortFor_"+appName.replace("-", "_"), + "paramContainerAppImageName": self._get_param_name_of_container_image(app), + "paramTargetPort": self._get_param_name_of_target_port(app), "moduleName": moduleName, "ingress": ingress, "isPublic": isPublic, diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 7a39d11d59c..337c5435008 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -81,6 +81,21 @@ def _get_storage_unique_name(self, disk_props): result = f"{storage_name}{hash_value}" return result[:32] # Ensure total length is no more than 32 + # get param name of paramContainerAppImageName + def _get_param_name_of_container_image(self, app): + appName = app['name'].split('/')[-1] + return "containerImageOf_"+appName.replace("-", "_") + + # get param name of paramTargetPort + def _get_param_name_of_target_port(self, app): + appName = app['name'].split('/')[-1] + 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 + class SourceDataWrapper: def __init__(self, source): self.source = source diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 84feb6ad278..6755113a39b 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -63,11 +63,10 @@ def _get_app_storage_configs(self, apps): # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") - paramContainerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name # print("storage_unique_name:", storage_unique_name) storage_config = { 'containerAppEnvStorageName': containerAppEnvStorageName, - 'paramContainerAppEnvStorageAccountKey': paramContainerAppEnvStorageAccountKey, + 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), 'storageName': storage_unique_name, 'shareName': share_name, 'accessMode': access_mode, diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index c972ccd5f4d..a16c4f2ce72 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -28,18 +28,14 @@ def transform_data(): "appName": appName, "moduleName": moduleName, "templateName": templateName, - "paramContainerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "paramTargetPort": "targetPortFor_"+appName.replace("-", "_"), + "paramContainerAppImageName": self._get_param_name_of_container_image(app), + "paramTargetPort": self._get_param_name_of_target_port(app), } if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] for disk_props in disks: - # Get the account name from storage map using storageId - storage_unique_name = self._get_storage_unique_name(disk_props) - # print("storage_unique_name:", storage_unique_name) - paramContainerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name storage_config = { - 'paramContainerAppEnvStorageAccountKey': paramContainerAppEnvStorageAccountKey, + 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), } storage_configs.append(storage_config) diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 6682c23d32a..10da30c7830 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -12,20 +12,15 @@ def transform_data(): appName = app['name'].split('/')[-1] apps_data.append({ "appName": appName, - "paramContainerAppImageName": "containerImageFor_"+appName.replace("-", "_"), - "paramTargetPort": "targetPortFor_"+appName.replace("-", "_"), + "paramContainerAppImageName": self._get_param_name_of_container_image(app), + "paramTargetPort": self._get_param_name_of_target_port(app), }) if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] for disk_props in disks: - # Get the account name from storage map using storageId - account_name = self._get_account_name(disk_props) - storage_unique_name = self._get_storage_unique_name(disk_props) - paramContainerAppEnvStorageAccountKey = "containerAppEnvStorageAccountKey_" + storage_unique_name - # print("storage_unique_name:", storage_unique_name) storage_config = { - 'paramContainerAppEnvStorageAccountKey': paramContainerAppEnvStorageAccountKey, - 'accountName': account_name, + 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), + 'accountName': self._get_account_name(disk_props), } storage_configs.append(storage_config) return { From 9005b96ad6a3329705a75076e250cd3c014ca1d2 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 13:33:59 +0800 Subject: [PATCH 68/98] rename method --- src/spring/azext_spring/migration/converter/base_converter.py | 4 ++-- .../azext_spring/migration/converter/environment_converter.py | 2 +- .../azext_spring/migration/converter/param_converter.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 337c5435008..85d50a8a9d4 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -60,7 +60,7 @@ def _get_storage_name(self, disk_props): storage_id = disk_props.get('storageId', '') return self._get_resource_name(storage_id) if storage_id else '' - def _get_account_name(self, disk_props): + def _get_storage_account_name(self, disk_props): storages = self.wrapper_data.get_storages() storage_map = { storage['name'].split('/')[-1]: storage['properties']['accountName'] @@ -71,7 +71,7 @@ def _get_account_name(self, disk_props): def _get_storage_unique_name(self, disk_props): storage_name = self._get_storage_name(disk_props) - account_name = self._get_account_name(disk_props) + account_name = self._get_storage_account_name(disk_props) share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 6755113a39b..69855be027c 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -55,7 +55,7 @@ def _get_app_storage_configs(self, apps): for disk_props in disks: # Get the account name from storage map using storageId storage_name = self._get_storage_name(disk_props) - account_name = self._get_account_name(disk_props) + account_name = self._get_storage_account_name(disk_props) share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') app_name = app['name'].split('/')[-1] readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 10da30c7830..0b223c4d5da 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -20,7 +20,7 @@ def transform_data(): for disk_props in disks: storage_config = { 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), - 'accountName': self._get_account_name(disk_props), + 'accountName': self._get_storage_account_name(disk_props), } storage_configs.append(storage_config) return { From 73e1a72a48d05ba07f4e5f06a61727fce10f00a3 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 13:36:54 +0800 Subject: [PATCH 69/98] add else for cors case --- .../azext_spring/migration/converter/gateway_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 4c1af09905b..1aad74fde10 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -86,7 +86,8 @@ def _get_filters(self, route_name, r): for f in r.get('filters'): if 'cors' in f.lower(): logger.warning(f"Mismatch: 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.") - filters.append(f) + else: + filters.append(f) return filters def _check_features(self, scg_properties): From 989ad15ebbf1ddd3d5208251dc348012faa70d28 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 13:52:15 +0800 Subject: [PATCH 70/98] refactor storage --- .../migration/converter/app_converter.py | 24 +++++------------ .../migration/converter/base_converter.py | 27 ++++++++++++++++--- .../converter/environment_converter.py | 15 +++-------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index bc8ebee0417..5647dbd722d 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -32,17 +32,15 @@ def transform_data_item(self, app): if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] for disk_props in disks: - storage_name = self._get_storage_name(disk_props) - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - storage_unique_name = self._get_storage_unique_name(disk_props) + volume_name = self._get_storage_name(disk_props) volumeMounts.append({ - "volumeName": storage_name, - "mountPath": mount_path, + "volumeName": volume_name, + "mountPath": self._get_storage_mount_path(disk_props), }) volumes.append({ - "volumeName": storage_name, - "storageName": storage_unique_name, - "mountOptions": self.get_mount_options(disk_props), + "volumeName": volume_name, + "storageName": self._get_storage_unique_name(disk_props), + "mountOptions": self._get_mount_options(disk_props), }) # print("Volume mounts: ", volumeMounts) # print("Volumes: ", volumes) @@ -64,16 +62,6 @@ def transform_data_item(self, app): "volumes": volumes, } - 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_template_name(self): return "app.bicep" diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 85d50a8a9d4..9186a89dd06 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -60,6 +60,16 @@ def _get_storage_name(self, disk_props): storage_id = disk_props.get('storageId', '') return self._get_resource_name(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 = { @@ -72,15 +82,24 @@ def _get_storage_account_name(self, disk_props): 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 = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') - mount_path = disk_props.get('customPersistentDiskProperties').get('mountPath') - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' + 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}" 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 + # get param name of paramContainerAppImageName def _get_param_name_of_container_image(self, app): appName = app['name'].split('/')[-1] diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 69855be027c..1bb577d4e5b 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -53,24 +53,17 @@ def _get_app_storage_configs(self, apps): if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] for disk_props in disks: - # Get the account name from storage map using storageId storage_name = self._get_storage_name(disk_props) - account_name = self._get_storage_account_name(disk_props) - share_name = disk_props.get('customPersistentDiskProperties', '').get('shareName', '') app_name = app['name'].split('/')[-1] - readOnly = disk_props.get('customPersistentDiskProperties', False).get('readOnly', False) - access_mode = 'ReadOnly' if readOnly else 'ReadWrite' # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) - storage_unique_name = self._get_storage_unique_name(disk_props) containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") - # print("storage_unique_name:", storage_unique_name) storage_config = { 'containerAppEnvStorageName': containerAppEnvStorageName, 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), - 'storageName': storage_unique_name, - 'shareName': share_name, - 'accessMode': access_mode, - 'accountName': account_name, + '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), } storage_configs.append(storage_config) # print("storage_configs:", storage_configs) From 09ff5f3de3fe37f4ec25f414746c8b08eb8767e9 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 14:06:31 +0800 Subject: [PATCH 71/98] extra method _get_resource_name_of_storage --- .../migration/converter/environment_converter.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 1bb577d4e5b..254679961bd 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -53,12 +53,9 @@ def _get_app_storage_configs(self, apps): if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] for disk_props in disks: - storage_name = self._get_storage_name(disk_props) - app_name = app['name'].split('/')[-1] # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) - containerAppEnvStorageName = (app_name + "_" + storage_name).replace("-", "_") storage_config = { - 'containerAppEnvStorageName': containerAppEnvStorageName, + 'containerAppEnvStorageName': self._get_resource_name_of_storage(app, disk_props), '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), @@ -68,3 +65,9 @@ def _get_app_storage_configs(self, apps): storage_configs.append(storage_config) # print("storage_configs:", storage_configs) return storage_configs + + # get resource name of containerAppEnvStorageName + def _get_resource_name_of_storage(self, app, disk_props): + storage_name = self._get_storage_name(disk_props) + app_name = app['name'].split('/')[-1] + return (app_name + "_" + storage_name).replace("-", "_") \ No newline at end of file From 1bd67f0c641edc8677ed6f41a990ceb6f250b527 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 14:51:50 +0800 Subject: [PATCH 72/98] extra method for get module name --- .../azext_spring/migration/converter/app_converter.py | 4 +--- .../azext_spring/migration/converter/base_converter.py | 10 ++++++++++ .../azext_spring/migration/converter/cert_converter.py | 5 ++--- .../azext_spring/migration/converter/main_converter.py | 6 ++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 5647dbd722d..80d904c0a49 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -17,7 +17,6 @@ def transform_data_item(self, app): appName = app['name'].split('/')[-1] envName = app['name'].split('/')[0] asa_deployments = self.wrapper_data.get_deployments_by_app(app['name']) - moduleName = appName.replace("-", "_") serviceBinds = self._get_service_bind(app, envName) deployments = self._get_deployments(asa_deployments) blueDeployment = deployments[0] if len(deployments) > 0 else {} @@ -26,7 +25,6 @@ def transform_data_item(self, app): ingress = self._get_ingress(app, tier) isPublic = app['properties'].get('public') identity = app.get('identity') - # print(f"App name: {appName}, Module name: {moduleName}, Ingress: {ingress}, IsPublic: {isPublic}, Identity: {identity}") volumeMounts = [] volumes = [] if 'properties' in app and 'customPersistentDisks' in app['properties']: @@ -48,7 +46,7 @@ def transform_data_item(self, app): "containerAppName": appName, "paramContainerAppImageName": self._get_param_name_of_container_image(app), "paramTargetPort": self._get_param_name_of_target_port(app), - "moduleName": moduleName, + "moduleName": self._get_app_module_name(app), "ingress": ingress, "isPublic": isPublic, "minReplicas": 1, diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 9186a89dd06..7266a816bc6 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -100,6 +100,16 @@ def _get_mount_options(self, disk_props): # print("Mount options: ", mountOptions) return mountOptions +# module name + def _get_app_module_name(self, app): + appName = app['name'].split('/')[-1] + return appName.replace("-", "_") + + def _get_cert_module_name(self, cert): + certName = cert['name'].split('/')[-1] + return "cert_" + certName.replace("-", "_") + +# param name # get param name of paramContainerAppImageName def _get_param_name_of_container_image(self, app): appName = app['name'].split('/')[-1] diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 790833eaf02..2d7a7f1e476 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -16,14 +16,13 @@ def transform_data(): def transform_data_item(self, cert): certName = cert['name'].split('/')[-1] - moduleName = "cert_" + certName.replace("-", "_") isKeyVaultCert = False - certKeyVault = self._get_cert_key_vault(cert) cert = { "certName": certName, - "moduleName": moduleName, + "moduleName": self._get_cert_module_name(cert), "certificateType": "ServerSSLCertificate", } + certKeyVault = self._get_cert_key_vault(cert) if certKeyVault: cert["certificateKeyVaultProperties"] = certKeyVault isKeyVaultCert = True diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index a16c4f2ce72..dacbf3070f2 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -10,11 +10,10 @@ def transform_data(): certs = [] for item in asa_certs: certName = item['name'].split('/')[-1] - moduleName = "cert_" + certName.replace("-", "_") templateName = f"{certName}_cert.bicep" certData = { "certName": certName, - "moduleName": moduleName, + "moduleName": self._get_cert_module_name(item), "templateName": templateName, } certs.append(certData) @@ -22,11 +21,10 @@ def transform_data(): apps_data = [] for app in apps: appName = app['name'].split('/')[-1] - moduleName = appName.replace("-", "_") templateName = f"{appName}_app.bicep" appData = { "appName": appName, - "moduleName": moduleName, + "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), From 28c2158cb958c74c629f543b7c11d955b8c80b83 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 14:52:19 +0800 Subject: [PATCH 73/98] extra method for get resource name --- .../migration/converter/app_converter.py | 13 +++------ .../migration/converter/base_converter.py | 27 ++++++++++++------- .../migration/converter/cert_converter.py | 13 +++++---- .../converter/environment_converter.py | 4 +-- .../migration/converter/gateway_converter.py | 2 +- .../converter/live_view_converter.py | 2 +- .../migration/converter/main_converter.py | 10 +++---- .../migration/converter/param_converter.py | 7 +++-- 8 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 80d904c0a49..f907ad14106 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -14,9 +14,8 @@ def transform_data(): super().__init__(source, transform_data) def transform_data_item(self, app): - appName = app['name'].split('/')[-1] - envName = app['name'].split('/')[0] - asa_deployments = self.wrapper_data.get_deployments_by_app(app['name']) + envName = self._get_parent_resource_name(app) + asa_deployments = self.wrapper_data.get_deployments_by_app(app) serviceBinds = self._get_service_bind(app, envName) deployments = self._get_deployments(asa_deployments) blueDeployment = deployments[0] if len(deployments) > 0 else {} @@ -43,7 +42,7 @@ def transform_data_item(self, app): # print("Volume mounts: ", volumeMounts) # print("Volumes: ", volumes) return { - "containerAppName": appName, + "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), @@ -63,9 +62,6 @@ def transform_data_item(self, app): def get_template_name(self): return "app.bicep" - def get_app_name(input_string): - return input_string.split('/')[-1] - def _get_service_bind(self, app, envName): service_bind = [] addon = app['properties'].get('addonConfigs') @@ -107,7 +103,6 @@ def _get_service_bind(self, app, envName): def _get_deployments(self, asa_deployments): deployments = [] for deployment in asa_deployments: - deployment_name = deployment['name'].split('/')[-1] 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', {}) @@ -119,7 +114,7 @@ def _get_deployments(self, asa_deployments): scale = deployment.get('properties', {}).get('deploymentSettings', {}).get('scale', {}) capacity = deployment.get('sku', {}).get('capacity') deployment = { - "name": deployment_name, + "name": self._get_resource_name(deployment), "env": self._convert_env(env), "livenessProbe": self._convert_probe(liveness_probe, tier), "readinessProbe": self._convert_probe(readiness_probe, tier), diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 7266a816bc6..7ff15a676c3 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -42,11 +42,19 @@ def generate_output(self, 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): + return resource['name'].split('/')[0] + # 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_resource_name(self, resource_id): + 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 @@ -56,9 +64,10 @@ def _get_resource_name(self, resource_id): # print(f"Resource name: {result}") return result +# storage def _get_storage_name(self, disk_props): storage_id = disk_props.get('storageId', '') - return self._get_resource_name(storage_id) if storage_id else '' + 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') @@ -73,7 +82,7 @@ def _get_storage_access_mode(self, disk_props): def _get_storage_account_name(self, disk_props): storages = self.wrapper_data.get_storages() storage_map = { - storage['name'].split('/')[-1]: storage['properties']['accountName'] + self._get_resource_name(storage): storage['properties']['accountName'] for storage in storages } storage_name = self._get_storage_name(disk_props) @@ -102,22 +111,22 @@ def _get_mount_options(self, disk_props): # module name def _get_app_module_name(self, app): - appName = app['name'].split('/')[-1] + appName = self._get_resource_name(app) return appName.replace("-", "_") def _get_cert_module_name(self, cert): - certName = cert['name'].split('/')[-1] + 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 = app['name'].split('/')[-1] + 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 = app['name'].split('/')[-1] + appName = self._get_resource_name(app) return "targetPortOf_"+appName.replace("-", "_") # get param name of paramContainerAppEnvStorageAccountKey @@ -165,9 +174,9 @@ def get_apps(self): def get_deployments(self): return self.get_resources_by_type('Microsoft.AppPlatform/Spring/apps/deployments') - def get_deployments_by_app(self, app_name): + def get_deployments_by_app(self, app): deployments = self.get_deployments() - return [deployment for deployment in deployments if deployment['name'].startswith(f"{app_name}/")] + return [deployment for deployment in deployments if deployment['name'].startswith(f"{app['name']}/")] def is_enterprise_tier(self): return self.get_asa_service()['sku']['tier'] == 'Enterprise' diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 2d7a7f1e476..f06f0e0a1da 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -15,22 +15,21 @@ def transform_data(): super().__init__(source, transform_data) def transform_data_item(self, cert): - certName = cert['name'].split('/')[-1] isKeyVaultCert = False - cert = { - "certName": certName, + 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["certificateKeyVaultProperties"] = certKeyVault + cert_data["certificateKeyVaultProperties"] = certKeyVault isKeyVaultCert = True else: - cert["value"] = "*" + cert_data["value"] = "*" isKeyVaultCert = False - cert["isKeyVaultCert"] = isKeyVaultCert - return cert + cert_data["isKeyVaultCert"] = isKeyVaultCert + return cert_data def get_template_name(self): return "cert.bicep" diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 254679961bd..b5f0d4ebe60 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -6,7 +6,7 @@ class EnvironmentConverter(BaseConverter): def __init__(self, source): def transform_data(): asa_service = self.wrapper_data.get_asa_service() - name = asa_service['name'].split('/')[-1] + name = self._get_resource_name(asa_service) apps = self.wrapper_data.get_apps() certs = self.wrapper_data.get_certificates() data = { @@ -69,5 +69,5 @@ def _get_app_storage_configs(self, apps): # get resource name of containerAppEnvStorageName def _get_resource_name_of_storage(self, app, disk_props): storage_name = self._get_storage_name(disk_props) - app_name = app['name'].split('/')[-1] + app_name = self._get_resource_name(app) return (app_name + "_" + storage_name).replace("-", "_") \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 1aad74fde10..cbc81f7faed 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -53,7 +53,7 @@ def _get_routes(self, routes): name_counter = {} if routes: for route in routes: - base_name = route['name'].split('/')[-1] + 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']: diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 8e52f2ee6bf..dd88d9900bc 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -4,7 +4,7 @@ class LiveViewConverter(BaseConverter): def __init__(self, source): def transform_data(): - live_view = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews')[0] + # live_view = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/applicationLiveViews')[0] name = "admin" configurations = [] replicas = 1 diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index dacbf3070f2..78ec013ed66 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -5,22 +5,22 @@ class MainConverter(BaseConverter): def __init__(self, source): def transform_data(): - apps = self.wrapper_data.get_apps() asa_certs = self.wrapper_data.get_certificates() certs = [] - for item in asa_certs: - certName = item['name'].split('/')[-1] + 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(item), + "moduleName": self._get_cert_module_name(cert), "templateName": templateName, } certs.append(certData) storage_configs = [] apps_data = [] + apps = self.wrapper_data.get_apps() for app in apps: - appName = app['name'].split('/')[-1] + appName = self._get_resource_name(app) templateName = f"{appName}_app.bicep" appData = { "appName": appName, diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 0b223c4d5da..2357d228ec2 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -5,13 +5,12 @@ class ParamConverter(BaseConverter): def __init__(self, source): def transform_data(): - self.apps = self.wrapper_data.get_apps() + apps = self.wrapper_data.get_apps() storage_configs = [] apps_data = [] - for app in self.apps: - appName = app['name'].split('/')[-1] + for app in apps: apps_data.append({ - "appName": appName, + "appName": self._get_resource_name(app), "paramContainerAppImageName": self._get_param_name_of_container_image(app), "paramTargetPort": self._get_param_name_of_target_port(app), }) From 7389ae9ff4c587c42596a252ced38a8ee43a284a Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Fri, 7 Mar 2025 16:36:06 +0800 Subject: [PATCH 74/98] fix blue green deployment issue --- .../migration/converter/app_converter.py | 68 +++++++++---------- .../migration/converter/base_converter.py | 18 +++++ .../migration/converter/readme_converter.py | 18 ++++- .../converter/service_registry_converter.py | 2 +- .../converter/templates/readme.md.j2 | 5 ++ 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index f907ad14106..b8ee990c933 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -14,13 +14,12 @@ def transform_data(): super().__init__(source, transform_data) def transform_data_item(self, app): - envName = self._get_parent_resource_name(app) - asa_deployments = self.wrapper_data.get_deployments_by_app(app) - serviceBinds = self._get_service_bind(app, envName) - deployments = self._get_deployments(asa_deployments) - blueDeployment = deployments[0] if len(deployments) > 0 else {} - greenDeployment = deployments[1] if len(deployments) > 1 else {} + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + blueDeployment = self._transform_deployment(blueDeployment) + greenDeployment = self.wrapper_data.get_green_deployment_by_app(app) + greenDeployment = self._transform_deployment(greenDeployment) tier = blueDeployment.get('sku', {}).get('tier') + serviceBinds = self._get_service_bind(app) ingress = self._get_ingress(app, tier) isPublic = app['properties'].get('public') identity = app.get('identity') @@ -53,7 +52,7 @@ def transform_data_item(self, app): "serviceBinds": serviceBinds, "blue": blueDeployment, "green": greenDeployment, - "isBlueGreen": len(deployments) > 1, + "isBlueGreen": self.wrapper_data.is_support_blue_green_deployment(app), "identity": identity, "volumeMounts": volumeMounts, "volumes": volumes, @@ -62,8 +61,9 @@ def transform_data_item(self, app): def get_template_name(self): return "app.bicep" - def _get_service_bind(self, app, envName): + def _get_service_bind(self, app): service_bind = [] + envName = self._get_parent_resource_name(app) addon = app['properties'].get('addonConfigs') if addon is None: @@ -100,34 +100,30 @@ def _get_service_bind(self, app, envName): # print(f"Service bind: {service_bind}") return service_bind - def _get_deployments(self, asa_deployments): - deployments = [] - for deployment in asa_deployments: - 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') - deployment = { - "name": self._get_resource_name(deployment), - "env": self._convert_env(env), - "livenessProbe": self._convert_probe(liveness_probe, tier), - "readinessProbe": self._convert_probe(readiness_probe, tier), - "startupProbe": self._convert_probe(startup_probe, tier), - "cpuCore": cpuCore, - "memorySize": self._get_memory_by_cpu(cpuCore) or memorySize, - "scale": self._convert_scale(scale), - "capacity": capacity, - } - deployments.append(deployment) - - # print(f"deployments: {deployments}") - return deployments + def _transform_deployment(self, deployment): + if deployment is None or deployment == {}: + 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), + "readinessProbe": self._convert_probe(readiness_probe, tier), + "startupProbe": self._convert_probe(startup_probe, tier), + "cpuCore": cpuCore, + "memorySize": self._get_memory_by_cpu(cpuCore) or memorySize, + "scale": self._convert_scale(scale), + "capacity": capacity, + } def _convert_env(self, env): env_list = [] diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 7ff15a676c3..e34b58cc539 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -178,6 +178,24 @@ 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'] == True] + return deployments[0] if deployments else {} + + 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'] == False] + return deployments[0] if deployments else {} + + def get_green_deployments(self): + deployments = self.get_deployments() + deployments = [deployment for deployment in deployments if deployment['properties']['active'] == False] + return deployments if deployments else [] + + def is_support_blue_green_deployment(self, app): + return len(self.get_deployments_by_app(app)) > 1 + def is_enterprise_tier(self): return self.get_asa_service()['sku']['tier'] == 'Enterprise' diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index b80c887d562..9bce994d222 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -5,9 +5,21 @@ class ReadMeConverter(BaseConverter): def __init__(self, source): def transform_data(): - # TODO: Implement the transform_data method - pass + greenDeployments = self.wrapper_data.get_green_deployments() + return { + "greenDeployments": self._transform_deployments(greenDeployments), + } super().__init__(source, transform_data) def get_template_name(self): - return "README.md" \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index f0650dcbad5..ce052a81d92 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -4,7 +4,7 @@ class ServiceRegistryConverter(BaseConverter): def __init__(self, source): def transform_data(): - service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') + # service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') name = f"eureka" configurations = [] replicas = 1 diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index d6aafcb085f..a9325c10773 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -106,7 +106,12 @@ Some properties and configurations are not included in the Bicep files and must - **Custom Domain**: If you have a custom domain configured in Azure Spring Apps, the 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. Then you can follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. - **Managed Identity**: If you have enabled a system-assigned managed identity in Azure Spring Apps applications, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. +{%- if data.greenDeployments %} - **Blue-green deployment**: If you 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/en-us/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/en-us/azure/spring-apps/basic-standard/how-to-staging-environment). +{%- endfor %} +{%- endif %} - **Auto scale**: If you have enabled auto scaling in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. From 4227f589c89fde89bee1af384b6a2a811b86cf68 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Sun, 9 Mar 2025 22:29:57 +0800 Subject: [PATCH 75/98] lint check --- .../migration/converter/acs_converter.py | 7 ++- .../migration/converter/app_converter.py | 28 ++++----- .../migration/converter/base_converter.py | 57 ++++++++++--------- .../migration/converter/cert_converter.py | 5 +- .../converter/config_server_converter.py | 12 ++-- .../migration/converter/conversion_context.py | 9 +-- .../converter/environment_converter.py | 3 +- .../migration/converter/eureka_converter.py | 3 +- .../migration/converter/gateway_converter.py | 9 ++- .../converter/live_view_converter.py | 3 +- .../migration/converter/main_converter.py | 3 +- .../migration/converter/param_converter.py | 1 + .../migration/converter/readme_converter.py | 5 +- .../converter/service_registry_converter.py | 5 +- .../migration/migration_operations.py | 2 +- .../recordings/test_persistent_storage.yaml | 2 +- 16 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 4d6e1a1493c..972dbf14212 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + # Concrete Converter Subclass for Config Server class ACSConverter(BaseConverter): @@ -21,7 +22,7 @@ class ACSConverter(BaseConverter): def __init__(self, source): def transform_data(): acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0] - name = f"config" + name = "config" configurations, params = self._get_configurations_and_params(acs) replicas = 2 return { @@ -45,7 +46,7 @@ def _get_configurations_and_params(self, acs): 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_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) @@ -78,4 +79,4 @@ 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) \ No newline at end of file + params.append(param_name) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index cabfaf31cbe..e2b6337c0db 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -7,9 +7,10 @@ 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): @@ -42,8 +43,6 @@ def transform_data_item(self, app): "storageName": self._get_storage_unique_name(disk_props), "mountOptions": self._get_mount_options(disk_props), }) - # print("Volume mounts: ", volumeMounts) - # print("Volumes: ", volumes) return { "containerAppName": self._get_resource_name(app), "paramContainerAppImageName": self._get_param_name_of_container_image(app), @@ -60,11 +59,11 @@ def transform_data_item(self, 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) @@ -73,13 +72,13 @@ def _get_service_bind(self, app): if addon is None: return None - if addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None \ - or addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None: + if (addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None) \ + or (addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None): service_bind.append({ "name": "bind-config", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" }) - if self.wrapper_data.is_enterprise_tier() != True and self.wrapper_data.is_support_configserver(): + if self.wrapper_data.is_enterprise_tier() is not True and self.wrapper_data.is_support_configserver(): # standard tier enabled config server and bind all apps automatically service_bind.append({ "name": "bind-config", @@ -90,7 +89,7 @@ def _get_service_bind(self, app): "name": "bind-eureka", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'eureka')" }) - if self.wrapper_data.is_enterprise_tier() != True and self.wrapper_data.is_support_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", @@ -101,7 +100,6 @@ def _get_service_bind(self, app): "name": "bind-sba", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'admin')" }) - # print(f"Service bind: {service_bind}") return service_bind def _transform_deployment(self, deployment): @@ -132,7 +130,6 @@ def _transform_deployment(self, deployment): def _convert_env(self, env): env_list = [] for key, value in env.items(): - env_list.append({ "name": key, "value": value @@ -164,16 +161,15 @@ def _get_memory_by_cpu(self, cpu): # create a method _convert_probe to convert the probe from the source to the target format def _convert_probe(self, probe, tier): - # print(f"probe: {probe}") if probe is None: return None - if probe.get("disableProbe") == True: - logger.debug(f"Probe is disabled") + 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']. + if initialDelaySeconds > 60: # Container 'undefined' 'Type' probe's InitialDelaySeconds must be in the range of ['0', '60']. initialDelaySeconds = 60 result['initialDelaySeconds'] = initialDelaySeconds periodSeconds = probe.get("periodSeconds", None) @@ -240,7 +236,7 @@ def _get_ingress(self, app, tier): "transport": ingress.get('backendProtocol').replace("Default", "auto"), "sessionAffinity": ingress.get('sessionAffinity').replace("Cookie", "sticky").replace("None", "none") } - + def _convert_scale(self, scale): return { "minReplicas": scale.get("minReplicas", 1), diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 344280d05fe..231276ef42f 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -11,6 +11,7 @@ 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 @@ -21,7 +22,7 @@ def __init__(self, source, transform_data): def convert(self): outputs = {} - outputs[self.get_template_name()] =self.generate_output(self.data) + outputs[self.get_template_name()] = self.generate_output(self.data) return outputs def convert_many(self): @@ -29,7 +30,7 @@ def convert_many(self): 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) + outputs[name + "_" + self.get_template_name()] = self.generate_output(data) return outputs @abstractmethod @@ -43,11 +44,12 @@ def generate_output(self, data): 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 + # common def _get_resource_name(self, resource): return resource['name'].split('/')[-1] @@ -86,7 +88,7 @@ def _get_storage_access_mode(self, disk_props): def _get_storage_account_name(self, disk_props): storages = self.wrapper_data.get_storages() storage_map = { - self._get_resource_name(storage): storage['properties']['accountName'] + self._get_resource_name(storage): storage['properties']['accountName'] for storage in storages } storage_name = self._get_storage_name(disk_props) @@ -105,8 +107,8 @@ def _get_storage_unique_name(self, disk_props): 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: + 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 @@ -126,25 +128,26 @@ def _get_cert_module_name(self, cert): # get param name of paramContainerAppImageName def _get_param_name_of_container_image(self, app): appName = self._get_resource_name(app) - return "containerImageOf_"+appName.replace("-", "_") + 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("-", "_") - + 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 + 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']) @@ -153,48 +156,48 @@ def is_support_configserver(self): def is_support_ssoconfigserver(self): return self.is_support_feature('Microsoft.AppPlatform/Spring/configServers') - + def is_support_acs(self): return self.is_support_feature('Microsoft.AppPlatform/Spring/configurationServices') - + 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_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/routeConfigs') - + 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'] == True] + deployments = [deployment for deployment in deployments if deployment['properties']['active'] is True] return deployments[0] if deployments else {} 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'] == False] + deployments = [deployment for deployment in deployments if deployment['properties']['active'] is False] return deployments[0] if deployments else {} - + def get_green_deployments(self): deployments = self.get_deployments() - deployments = [deployment for deployment in deployments if deployment['properties']['active'] == False] + deployments = [deployment for deployment in deployments if deployment['properties']['active'] is False] return deployments if deployments else [] def is_support_blue_green_deployment(self, app): @@ -208,22 +211,22 @@ def is_vnet(self): 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') \ No newline at end of file + return self.get_resources_by_type('Microsoft.AppPlatform/Spring/storages') diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index cecb5955e7c..7eb37c585f9 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -7,6 +7,7 @@ logger = get_logger(__name__) + # Concrete Converter Subclass for certificate class CertConverter(BaseConverter): @@ -33,11 +34,11 @@ def transform_data_item(self, cert): cert_data["value"] = "*" isKeyVaultCert = False cert_data["isKeyVaultCert"] = isKeyVaultCert - return cert_data + 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": diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index 1c237c87859..7230d218943 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -6,6 +6,8 @@ from knack.log import get_logger logger = get_logger(__name__) + + # Concrete Converter Subclass for Config Server class ConfigServerConverter(BaseConverter): @@ -23,7 +25,7 @@ class ConfigServerConverter(BaseConverter): def __init__(self, source): def transform_data(): configServer = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configServers')[0] - name = f"config" + name = "config" configurations, params = self._get_configurations_and_params(configServer) replicas = 2 return { @@ -36,17 +38,17 @@ def 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_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) @@ -80,4 +82,4 @@ 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) \ No newline at end of file + 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 index 54520358c08..f63136acea4 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -4,7 +4,6 @@ # -------------------------------------------------------------------------------------------- import os -from abc import ABC, abstractmethod from knack.log import get_logger from .base_converter import ConverterTemplate, SourceDataWrapper from .environment_converter import EnvironmentConverter @@ -22,6 +21,7 @@ logger = get_logger(__name__) + # Context Class class ConversionContext: def __init__(self, source): @@ -72,7 +72,7 @@ def run_converters(self): if self.data_wrapper.is_support_serviceregistry(): converted_contents.update(self.get_converter(ServiceRegistryConverter).convert()) # logger.debug(f"converted_contents for Service Registry:\n{converted_contents.get(self.get_converter(ServiceRegistryConverter).get_template_name())}") - else: # Basic Tier or Standard Tier + else: # Basic Tier or Standard Tier converted_contents.update(self.get_converter(EurekaConverter).convert()) # logger.debug(f"converted_contents for Eureka:\n{converted_contents.get(self.get_converter(EurekaConverter).get_template_name())}") @@ -80,7 +80,7 @@ def run_converters(self): apps = self.get_converter(AppConverter).convert_many() converted_contents.update(apps) # for app in apps.keys(): - # logger.debug(f"converted_contents for App {app}:\n{converted_contents.get(app)}") + # logger.debug(f"converted_contents for App {app}:\n{converted_contents.get(app)}") # Param Converter converted_contents.update(self.get_converter(ParamConverter).convert()) @@ -104,6 +104,3 @@ def save_to_files(self, converted_contents, output_path): 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 index f6f579d0b0d..ced33e1843b 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + # Concrete Subclass for Container App Environment class EnvironmentConverter(BaseConverter): @@ -74,4 +75,4 @@ def _get_app_storage_configs(self, apps): def _get_resource_name_of_storage(self, app, disk_props): storage_name = self._get_storage_name(disk_props) app_name = self._get_resource_name(app) - return (app_name + "_" + storage_name).replace("-", "_") \ No newline at end of file + return (app_name + "_" + storage_name).replace("-", "_") diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 1278b5b91e3..50fb8a89b8d 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + class EurekaConverter(BaseConverter): def __init__(self, source): @@ -21,5 +22,3 @@ def 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 index 04f9b098a03..f2c679557ef 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -4,10 +4,10 @@ # -------------------------------------------------------------------------------------------- from knack.log import get_logger from .base_converter import BaseConverter -from knack.log import get_logger logger = get_logger(__name__) + class GatewayConverter(BaseConverter): DEFAULT_NAME = "default" @@ -26,11 +26,10 @@ def transform_data(): self._check_features(gateway.get('properties', {})) return { "routes": routes, - "gatewayName": f"gateway", + "gatewayName": "gateway", "configurations": configurations, - "replicas": replicas, - "routes": routes, - } + "replicas": replicas, + } self.client = client self.resource_group = resource_group self.service = service diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index e35635b54f4..3817fdea8df 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + class LiveViewConverter(BaseConverter): def __init__(self, source): @@ -21,5 +22,3 @@ def 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 index fb6b65857e9..93c143ff458 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -4,7 +4,8 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter -# Concrete Converter Subclass for Read Me + +# Concrete Converter Subclass for main class MainConverter(BaseConverter): def __init__(self, source): diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index 63a7ccc446d..fa965c97a60 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + # Concrete Converter Subclass for paramter class ParamConverter(BaseConverter): diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index aba72c4a38e..4e96f8d153c 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + # Concrete Converter Subclass for Read Me class ReadMeConverter(BaseConverter): @@ -17,7 +18,7 @@ def transform_data(): def get_template_name(self): return "README.md" - + def _transform_deployments(self, deployments): deployments_data = [] for deployment in deployments: @@ -26,4 +27,4 @@ def _transform_deployments(self, deployments): "name": self._get_resource_name(deployment), } deployments_data.append(deployment_data) - return deployments_data \ No newline at end of file + return deployments_data diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index cb4877c35d4..409fcbe0070 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -4,12 +4,13 @@ # -------------------------------------------------------------------------------------------- from .base_converter import BaseConverter + class ServiceRegistryConverter(BaseConverter): def __init__(self, source): def transform_data(): # service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') - name = f"eureka" + name = "eureka" configurations = [] replicas = 1 @@ -22,5 +23,3 @@ def transform_data(): def get_template_name(self): return "eureka.bicep" - - diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 845b0e3ab19..670f1e9135b 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -64,7 +64,7 @@ def export_asa_arm_template(cmd, resource_group, service): 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'): 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"}]}' From c8d0bfdabaf3b66be2499fa8f20e462b7d1fb2c9 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Mon, 10 Mar 2025 11:00:07 +0800 Subject: [PATCH 76/98] lint check --- .../azext_spring/migration/converter/app_converter.py | 6 ++++-- .../azext_spring/migration/converter/gateway_converter.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index e2b6337c0db..6f3337105b4 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -72,8 +72,10 @@ def _get_service_bind(self, app): if addon is None: return None - if (addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None) \ - or (addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None): + if ( + (addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None) + or (addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None) + ): service_bind.append({ "name": "bind-config", "serviceId": f"resourceId('Microsoft.App/managedEnvironments/javaComponents', '{envName}', 'config')" diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index f2c679557ef..9fea3a4f680 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -28,7 +28,7 @@ def transform_data(): "routes": routes, "gatewayName": "gateway", "configurations": configurations, - "replicas": replicas, + "replicas": replicas, } self.client = client self.resource_group = resource_group From 6af65aac8539ece1d1528ff1998cb8122b2f698d Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 00:06:23 +0800 Subject: [PATCH 77/98] add integration test for asa export command for aca migration --- .../latest/recordings/test_asa_export.yaml | 294 ++++++++++++++++++ .../tests/latest/test_asa_export.py | 34 ++ 2 files changed, 328 insertions(+) create mode 100644 src/spring/azext_spring/tests/latest/recordings/test_asa_export.yaml create mode 100644 src/spring/azext_spring/tests/latest/test_asa_export.py 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/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') + + From a6827b5bf250ae54c649bf128c98d56cf13c7241 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 10:31:09 +0800 Subject: [PATCH 78/98] fix typo --- src/spring/azext_spring/migration/converter/base_converter.py | 4 ++-- .../azext_spring/migration/converter/conversion_context.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 231276ef42f..0950717cfde 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -152,9 +152,9 @@ 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_ssoconfigserver() or self.is_support_acs() + return self.is_support_ossconfigserver() or self.is_support_acs() - def is_support_ssoconfigserver(self): + def is_support_ossconfigserver(self): return self.is_support_feature('Microsoft.AppPlatform/Spring/configServers') def is_support_acs(self): diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index f63136acea4..b2e539923cb 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -55,7 +55,7 @@ def run_converters(self): # logger.debug(f"converted_contents for gateway:\n{converted_contents.get(self.get_converter(GatewayConverter).get_template_name())}") # Config Server and ACS Converter - if self.data_wrapper.is_support_ssoconfigserver(): + if self.data_wrapper.is_support_ossconfigserver(): converted_contents.update(self.get_converter(ConfigServerConverter).convert()) # logger.debug(f"converted_contents for config server:\n{converted_contents.get(self.get_converter(ConfigServerConverter).get_template_name())}") elif self.data_wrapper.is_support_acs(): From ea32e280c2c0f57181e0e67d41743a6c1a572b44 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 11:02:47 +0800 Subject: [PATCH 79/98] refactor _get_service_bind method --- .../migration/converter/app_converter.py | 14 +++---------- .../migration/converter/base_converter.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 6f3337105b4..00dbbc5907d 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -67,26 +67,18 @@ def get_template_name(self): def _get_service_bind(self, app): service_bind = [] envName = self._get_parent_resource_name(app) - addon = app['properties'].get('addonConfigs') - - if addon is None: - return None - - if ( - (addon.get('applicationConfigurationService') is not None and addon['applicationConfigurationService'].get('resourceId') is not None) - or (addon.get('configServer') is not None and addon['configServer'].get('resourceId') is not None) - ): + 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_configserver(): + 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 addon.get('serviceRegistry') is not None and addon['serviceRegistry'].get('resourceId') is not None: + 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')" diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 0950717cfde..34c082eb3f8 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -154,18 +154,39 @@ def is_support_feature(self, feature): 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') From 0b3514d2655e8f27403bb8132e6e35f63071f46e Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 11:33:08 +0800 Subject: [PATCH 80/98] add depends on list --- .../migration/converter/main_converter.py | 17 +++++++++++++++++ .../migration/converter/templates/main.bicep.j2 | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 93c143ff458..f1b7e7f2280 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -33,6 +33,7 @@ def transform_data(): "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), } if 'properties' in app and 'customPersistentDisks' in app['properties']: disks = app['properties']['customPersistentDisks'] @@ -58,3 +59,19 @@ def 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 \ 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 index 4c3b5ae09c8..339b21c6211 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -43,6 +43,13 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { {%- for item in data.apps %} module {{ item.moduleName }} '{{ item.templateName }}' = { name: '{{ item.appName }}-Deployment' + {%- if item.dependsOns %} + dependsOn: [ + {%- for dependsOn in item.dependsOns %} + {{ dependsOn }} + {%- endfor %} + ] + {%- endif %} params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId workloadProfileName: workloadProfileName From a35c45ca0301f0bb5a7657f585f55f47d9c14889 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Tue, 11 Mar 2025 12:13:41 +0800 Subject: [PATCH 81/98] readme dynamically --- .../migration/converter/base_converter.py | 19 ++++ .../migration/converter/readme_converter.py | 42 ++++++++- .../converter/templates/param.bicepparam.j2 | 2 +- .../converter/templates/readme.md.j2 | 92 ++++++++++++++----- 4 files changed, 128 insertions(+), 27 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 34c082eb3f8..70156c2c038 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -221,9 +221,28 @@ def get_green_deployments(self): 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' diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 4e96f8d153c..1583faa1ff0 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -10,10 +10,36 @@ class ReadMeConverter(BaseConverter): def __init__(self, source): def transform_data(): - greenDeployments = self.wrapper_data.get_green_deployments() - return { - "greenDeployments": self._transform_deployments(greenDeployments), + is_vnet = self.wrapper_data.is_vnet() + build_results_deployments = self.wrapper_data.get_build_results_deployments() + container_deployments = self.wrapper_data.get_container_deployments() + is_support_configserver = self.wrapper_data.is_support_ossconfigserver() + custom_domains = self.wrapper_data.get_custom_domains() + apps = self.wrapper_data.get_apps() + has_apps = len(apps) > 0 + 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 + auto_scale_enabled_deployments = [] #todo + system_assigned_identity_apps = [] #todo + + data = { + "isVnet": is_vnet, + "containerApps": container_deployments, + "buildResultsApps": build_results_deployments, + "hasApps": has_apps, + "isSupportConfigServer": is_support_configserver, + "customDomains": self._transform_domains(custom_domains), + "keyVaultCerts": keyvault_certs, + "contentCerts": content_certs, + "greenDeployments": self._transform_deployments(green_deployments), + "autoScaleEnabledApps": auto_scale_enabled_deployments, + "shouldSystemAssignedIdentityEnabled": should_system_assigned_identity_enabled, + "systemAssignedIdentityApps": system_assigned_identity_apps, } + print(f"ReadMeConverter data: {data}") + return data super().__init__(source, transform_data) def get_template_name(self): @@ -28,3 +54,13 @@ def _transform_deployments(self, deployments): } 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 diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index 7354e52661e..a5a99a6a04a 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -6,7 +6,7 @@ param minNodes = 1 param maxNodes = 10 {%- if data.isVnet == true %} -// Provide the full resource ID of the existing subnet. +// 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 %} diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index a9325c10773..966e3f7aa50 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -12,22 +12,6 @@ Before getting started, make sure you have the following: ## Steps to Deploy Azure Container Apps Using the Bicep Files -### Check Output Message - -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." - ``` - ### Review the Bicep Files After generating the Bicep files, the specified output directory will contain the following files and related resource definitions: @@ -44,22 +28,46 @@ These Bicep files enable quick deployment of the Azure Container Apps environmen 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/en-us/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 +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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-build-overview), and learn how to [deploy them](https://learn.microsoft.com/en-us/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/en-us/azure/container-apps/managed-identity-image-pull?tabs=bash&pivots=portal). +For following applications that are built by Azure Spring Apps, you need to build the image and deploy to Azure Container Apps. +{%- for app in data.buildResultsApps %} + - `{{app.name}}` +{%- endfor %} +{%- endif %} + +{%- if data.isSupportConfigServer %} #### Managed Components Configuration -If you are migrating Application Configuration Service or Config Server, you need to provide the credentials for the remote Git repository in the `config_server.bicep` file. +You need to update the GIT repoistories for the remote Git repository in the `config_server.bicep` file, such as providing the credentials. +{%- endif %} #### Mismatch during migration -During the migration process, you may encounter warnings or errors caused by property mismatches between Azure Spring Apps and Azure Container Apps. If the generated Bicep files don't fully meet your needs, you can customize the resource definitions before deployment. +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: @@ -104,15 +112,53 @@ You can check the deployment status either in the CLI output or in the Azure Por Some properties and configurations are not included in the Bicep files and must be manually updated. -- **Custom Domain**: If you have a custom domain configured in Azure Spring Apps, the 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. Then you can follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. -- **Managed Identity**: If you have enabled a system-assigned managed identity in Azure Spring Apps applications, your Azure Container Apps instance will also have a system-assigned managed identity, but you need to reassign the corresponding roles to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. +{%- if data.shouldSystemAssignedIdentityEnabled %} +### **TLS certificate** +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.keyvault_certs %} + - `{{cert.name}}` +{%- endfor %} + +Following content type certificates should be manually uploaded to Azure Container Apps environment: +{%- for cert in data.contentCerts %} + - `{{cert.name}}` +{%- endfor %} +{%- 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/en-us/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.system_assigned_identity_apps %} +### 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 to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. +{%- for app in data.system_assigned_identity_apps %} + - `{{app.name}}` +{%- endfor %} +{%- endif %} + {%- if data.greenDeployments %} -- **Blue-green deployment**: If you 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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. +### 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/en-us/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/en-us/azure/spring-apps/basic-standard/how-to-staging-environment). {%- endfor %} {%- endif %} -- **Auto scale**: If you have enabled auto scaling in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. + +{%- if data.auto_scale_enabled_deployments %} +### Auto scale +You have enabled auto scaling for following applications in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. +{%- for deployment in data.autoScaleEnabledApps %} + - `{{deployment.appName}}/{{deployment.name}}` +{%- endfor %} +{%- endif %} + +### Monitoring +The migration has enabled logging and monitoring in Azure Container Apps. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. Refer to the [migration documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) for more detailed feature migration steps. \ No newline at end of file From c5e94e6d9e82baf9815f11bc7b4587c4531c8b3c Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 12:42:11 +0800 Subject: [PATCH 82/98] fix [0] Array out of bounds issue --- src/spring/azext_spring/migration/converter/base_converter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 70156c2c038..87271db2dd1 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -18,15 +18,17 @@ class ConverterTemplate(ABC): def __init__(self, source, transform_data): self.wrapper_data = SourceDataWrapper(source) - self.data = transform_data() + self.transform_data = transform_data def convert(self): outputs = {} + self.data = self.transform_data() outputs[self.get_template_name()] = self.generate_output(self.data) return outputs def convert_many(self): outputs = {} + self.data = self.transform_data() for item in self.data: name = item['name'].split('/')[-1] data = self.transform_data_item(item) From f04bd90b8d28328ce469f9e39b3586218fdae7e5 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 12:52:10 +0800 Subject: [PATCH 83/98] check data --- .../migration/converter/base_converter.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 87271db2dd1..9e29c305b30 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -23,16 +23,18 @@ def __init__(self, source, transform_data): def convert(self): outputs = {} self.data = self.transform_data() - outputs[self.get_template_name()] = self.generate_output(self.data) + if self.data is not None: + outputs[self.get_template_name()] = self.generate_output(self.data) return outputs def convert_many(self): outputs = {} self.data = self.transform_data() - 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) + 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 From adba893f1db5551b3450bf4f1470fd3998275ab1 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 13:10:22 +0800 Subject: [PATCH 84/98] move skip logic into converters --- .../migration/converter/acs_converter.py | 23 +++++++----- .../converter/config_server_converter.py | 23 +++++++----- .../migration/converter/eureka_converter.py | 19 ++++++---- .../migration/converter/gateway_converter.py | 37 ++++++++++--------- .../converter/live_view_converter.py | 21 ++++++----- .../converter/service_registry_converter.py | 24 +++++++----- 6 files changed, 84 insertions(+), 63 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 972dbf14212..983da34963c 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -21,16 +21,19 @@ class ACSConverter(BaseConverter): def __init__(self, source): def transform_data(): - 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 - } + 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): diff --git a/src/spring/azext_spring/migration/converter/config_server_converter.py b/src/spring/azext_spring/migration/converter/config_server_converter.py index 7230d218943..c5fef61545d 100644 --- a/src/spring/azext_spring/migration/converter/config_server_converter.py +++ b/src/spring/azext_spring/migration/converter/config_server_converter.py @@ -24,16 +24,19 @@ class ConfigServerConverter(BaseConverter): def __init__(self, source): def transform_data(): - 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 - } + 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): diff --git a/src/spring/azext_spring/migration/converter/eureka_converter.py b/src/spring/azext_spring/migration/converter/eureka_converter.py index 50fb8a89b8d..3744d9092cd 100644 --- a/src/spring/azext_spring/migration/converter/eureka_converter.py +++ b/src/spring/azext_spring/migration/converter/eureka_converter.py @@ -9,15 +9,18 @@ class EurekaConverter(BaseConverter): def __init__(self, source): def transform_data(): - name = "eureka" - configurations = [] - replicas = 1 + if self.wrapper_data.is_enterprise_tier() is False: + name = "eureka" + configurations = [] + replicas = 1 - return { - "eurekaName": name, - "configurations": configurations, - "replicas": replicas - } + return { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + else: + return None super().__init__(source, transform_data) def get_template_name(self): diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 9fea3a4f680..471ae059eff 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -13,23 +13,26 @@ class GatewayConverter(BaseConverter): def __init__(self, source, client, resource_group, service): def transform_data(): - 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', {})) - return { - "routes": routes, - "gatewayName": "gateway", - "configurations": configurations, - "replicas": replicas, - } + 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', {})) + return { + "routes": routes, + "gatewayName": "gateway", + "configurations": configurations, + "replicas": replicas, + } + else: + return None self.client = client self.resource_group = resource_group self.service = service diff --git a/src/spring/azext_spring/migration/converter/live_view_converter.py b/src/spring/azext_spring/migration/converter/live_view_converter.py index 3817fdea8df..0a83cd20457 100644 --- a/src/spring/azext_spring/migration/converter/live_view_converter.py +++ b/src/spring/azext_spring/migration/converter/live_view_converter.py @@ -9,15 +9,18 @@ class LiveViewConverter(BaseConverter): def __init__(self, source): def transform_data(): - # 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 - } + 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): diff --git a/src/spring/azext_spring/migration/converter/service_registry_converter.py b/src/spring/azext_spring/migration/converter/service_registry_converter.py index 409fcbe0070..f5a6faab5f6 100644 --- a/src/spring/azext_spring/migration/converter/service_registry_converter.py +++ b/src/spring/azext_spring/migration/converter/service_registry_converter.py @@ -9,16 +9,22 @@ class ServiceRegistryConverter(BaseConverter): def __init__(self, source): def transform_data(): - # service_registry = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/serviceRegistries') - name = "eureka" - configurations = [] - replicas = 1 + 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 - } + return { + "eurekaName": name, + "configurations": configurations, + "replicas": replicas + } + else: + return None + else: + return None super().__init__(source, transform_data) def get_template_name(self): From 22c0f05efb349373942dfb88fdf1e3a08fc35639 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 14:04:48 +0800 Subject: [PATCH 85/98] refactor run_converters method --- .../migration/converter/base_converter.py | 18 ++++-- .../migration/converter/conversion_context.py | 57 +------------------ 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 9e29c305b30..2ee17447b24 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -21,15 +21,25 @@ def __init__(self, source, transform_data): self.transform_data = transform_data def convert(self): - outputs = {} + result = {} self.data = self.transform_data() - if self.data is not None: + 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): + def _convert_many(self): outputs = {} - self.data = self.transform_data() 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] diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index b2e539923cb..3e983d0bcd9 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -39,60 +39,9 @@ def get_converter(self, converter_type: type): def run_converters(self): converted_contents = {} - # Cert Converter - certs = self.get_converter(CertConverter).convert_many() - converted_contents.update(certs) - # for cert in certs.keys(): - # logger.debug(f"converted_contents for cert {cert}:\n{converted_contents.get(cert)}") - - # Environment Converter - converted_contents.update(self.get_converter(EnvironmentConverter).convert()) - # logger.debug(f"converted_contents for environment:\n{converted_contents.get(self.get_converter(EnvironmentConverter).get_template_name())}") - - # Gateway Converter - if self.data_wrapper.is_support_gateway(): - converted_contents.update(self.get_converter(GatewayConverter).convert()) - # logger.debug(f"converted_contents for gateway:\n{converted_contents.get(self.get_converter(GatewayConverter).get_template_name())}") - - # Config Server and ACS Converter - if self.data_wrapper.is_support_ossconfigserver(): - converted_contents.update(self.get_converter(ConfigServerConverter).convert()) - # logger.debug(f"converted_contents for config server:\n{converted_contents.get(self.get_converter(ConfigServerConverter).get_template_name())}") - elif self.data_wrapper.is_support_acs(): - converted_contents.update(self.get_converter(ACSConverter).convert()) - # logger.debug(f"converted_contents for Application Configuration Service:\n{converted_contents.get(self.get_converter(ACSConverter).get_template_name())}") - - # Live View Converter - if self.data_wrapper.is_support_sba(): - converted_contents.update(self.get_converter(LiveViewConverter).convert()) - # logger.debug(f"converted_contents for Live View:\n{converted_contents.get(self.get_converter(LiveViewConverter).get_template_name())}") - - # Service Registry and Eureka Converter - if self.data_wrapper.is_enterprise_tier(): - if self.data_wrapper.is_support_serviceregistry(): - converted_contents.update(self.get_converter(ServiceRegistryConverter).convert()) - # logger.debug(f"converted_contents for Service Registry:\n{converted_contents.get(self.get_converter(ServiceRegistryConverter).get_template_name())}") - else: # Basic Tier or Standard Tier - converted_contents.update(self.get_converter(EurekaConverter).convert()) - # logger.debug(f"converted_contents for Eureka:\n{converted_contents.get(self.get_converter(EurekaConverter).get_template_name())}") - - # App Converter - apps = self.get_converter(AppConverter).convert_many() - converted_contents.update(apps) - # for app in apps.keys(): - # logger.debug(f"converted_contents for App {app}:\n{converted_contents.get(app)}") - - # Param Converter - converted_contents.update(self.get_converter(ParamConverter).convert()) - # logger.debug(f"converted_contents for Param:\n{converted_contents.get(self.get_converter(ParamConverter).get_template_name())}") - - # ReadMe Converter - converted_contents.update(self.get_converter(ReadMeConverter).convert()) - # logger.debug(f"converted_contents for ReadMe:\n{converted_contents.get(self.get_converter(ReadMeConverter).get_template_name())}") - - # Main Converter - converted_contents.update(self.get_converter(MainConverter).convert()) - # logger.debug(f"converted_contents for Main:\n{converted_contents.get(self.get_converter(MainConverter).get_template_name())}") + for converter in self.converters: + items = converter.convert() + converted_contents.update(items) return converted_contents def save_to_files(self, converted_contents, output_path): From 959fbfaf3a08ba5568cb4aab21d60223a1d83f66 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 14:42:43 +0800 Subject: [PATCH 86/98] fix README.j2 No such file error --- src/spring/azext_spring/migration/converter/base_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 2ee17447b24..e39885ecf66 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -53,7 +53,7 @@ def get_template_name(self): def generate_output(self, data): script_dir = os.path.dirname(os.path.abspath(__file__)) - template_name = self.get_template_name() + 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) From 13d5af41800e722f81a8e9f3b9c51fd0e91f00fa Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Tue, 11 Mar 2025 14:44:25 +0800 Subject: [PATCH 87/98] refine --- .../migration/converter/base_converter.py | 7 ++++--- .../migration/converter/main_converter.py | 2 +- .../migration/converter/readme_converter.py | 8 ++++---- .../converter/templates/readme.md.j2 | 20 +++++++++++-------- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 2ee17447b24..4059ae3a364 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -26,7 +26,7 @@ def convert(self): 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)}") + # 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())}") @@ -68,7 +68,8 @@ def _get_resource_name(self, resource): return resource['name'].split('/')[-1] def _get_parent_resource_name(self, resource): - return resource['name'].split('/')[0] + 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/', '', '')] @@ -198,7 +199,7 @@ def is_support_serviceregistry(self): def is_support_serviceregistry_for_app(self, app): addon = app['properties'].get('addonConfigs') if addon is None: - return False + return False return addon.get('serviceRegistry') is not None and addon['serviceRegistry'].get('resourceId') is not None def is_support_sba(self): diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index f1b7e7f2280..072bbcf438b 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -74,4 +74,4 @@ def _get_depends_on_list(self, app): service_bind.append("managedEureka") if self.wrapper_data.is_support_sba(): service_bind.append("managedSpringBootAdmin") - return service_bind \ No newline at end of file + return service_bind diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 1583faa1ff0..f17366dbfac 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -16,21 +16,21 @@ def transform_data(): is_support_configserver = self.wrapper_data.is_support_ossconfigserver() custom_domains = self.wrapper_data.get_custom_domains() apps = self.wrapper_data.get_apps() - has_apps = len(apps) > 0 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 - auto_scale_enabled_deployments = [] #todo - system_assigned_identity_apps = [] #todo + auto_scale_enabled_deployments = [] # todo + system_assigned_identity_apps = [] # todo data = { "isVnet": is_vnet, "containerApps": container_deployments, "buildResultsApps": build_results_deployments, - "hasApps": has_apps, + "hasApps": len(apps) > 0, "isSupportConfigServer": 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), diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index 966e3f7aa50..3ac75d82228 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -1,6 +1,8 @@ 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. -After deploying the Bicep files, some configurations must be updated manually by following the steps outlined in [Azure public documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) after deploying the Bicep files. +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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview). ## Prerequisites @@ -109,23 +111,25 @@ You can check the deployment status either in the CLI output or in the Azure Por | 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. | ## Post-Deployment Configuration - Some properties and configurations are not included in the Bicep files and must be manually updated. -{%- if data.shouldSystemAssignedIdentityEnabled %} +{% 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.keyvault_certs %} - `{{cert.name}}` {%- endfor %} - +{%- endif %} +{% if data.contentCerts %} Following content type certificates should be manually uploaded to Azure Container Apps environment: {%- for cert in data.contentCerts %} - `{{cert.name}}` {%- endfor %} {%- endif %} +{%- endif %} -{%- if data.customDomains %} +{% 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/en-us/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 %} @@ -133,7 +137,7 @@ You have custom domains configured in Azure Spring Apps. You can follow the step {%- endfor %} {%- endif %} -{%- if data.system_assigned_identity_apps %} +{% if data.system_assigned_identity_apps %} ### 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 to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. @@ -142,7 +146,7 @@ Following applications have enabled system-assigned managed identity in Azure Sp {%- endfor %} {%- endif %} -{%- if data.greenDeployments %} +{% 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/en-us/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 %} @@ -150,7 +154,7 @@ Following applications have enabled blue-green deployments in Azure Spring Apps, {%- endfor %} {%- endif %} -{%- if data.auto_scale_enabled_deployments %} +{% if data.auto_scale_enabled_deployments %} ### Auto scale You have enabled auto scaling for following applications in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. {%- for deployment in data.autoScaleEnabledApps %} From b5230cb11d345a32119031cc48b02b3bb4c19192 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Tue, 11 Mar 2025 14:49:09 +0800 Subject: [PATCH 88/98] create converter class when needed --- .../migration/converter/conversion_context.py | 28 ++++--------------- .../migration/migration_operations.py | 24 ++++++++-------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/conversion_context.py b/src/spring/azext_spring/migration/converter/conversion_context.py index 3e983d0bcd9..27bf23c7fb3 100644 --- a/src/spring/azext_spring/migration/converter/conversion_context.py +++ b/src/spring/azext_spring/migration/converter/conversion_context.py @@ -5,19 +5,6 @@ import os from knack.log import get_logger -from .base_converter import ConverterTemplate, SourceDataWrapper -from .environment_converter import EnvironmentConverter -from .app_converter import AppConverter -from .readme_converter import ReadMeConverter -from .main_converter import MainConverter -from .param_converter import ParamConverter -from .gateway_converter import GatewayConverter -from .eureka_converter import EurekaConverter -from .service_registry_converter import ServiceRegistryConverter -from .config_server_converter import ConfigServerConverter -from .acs_converter import ACSConverter -from .live_view_converter import LiveViewConverter -from .cert_converter import CertConverter logger = get_logger(__name__) @@ -25,21 +12,16 @@ # Context Class class ConversionContext: def __init__(self, source): - self.data_wrapper = SourceDataWrapper(source) + self.source = source self.converters = [] - def add_converter(self, converter: ConverterTemplate): - self.converters.append(converter) - - def get_converter(self, converter_type: type): - for converter in self.converters: - if isinstance(converter, converter_type): - return converter - raise ValueError(f"Unknown converter type: {converter_type}") + def register_converter(self, converter_class): + self.converters.append(converter_class) def run_converters(self): converted_contents = {} - for converter in self.converters: + for converter_class in self.converters: + converter = converter_class(self.source) items = converter.convert() converted_contents.update(items) return converted_contents diff --git a/src/spring/azext_spring/migration/migration_operations.py b/src/spring/azext_spring/migration/migration_operations.py index 670f1e9135b..5f1e70ef4d9 100644 --- a/src/spring/azext_spring/migration/migration_operations.py +++ b/src/spring/azext_spring/migration/migration_operations.py @@ -30,18 +30,18 @@ def migration_aca_start(cmd, client, resource_group, service, output_folder): # Create context and add converters context = ConversionContext(asa_arm) - context.add_converter(MainConverter(asa_arm)) - context.add_converter(EnvironmentConverter(asa_arm)) - context.add_converter(AppConverter(asa_arm)) - context.add_converter(GatewayConverter(asa_arm, client, resource_group, service)) - context.add_converter(EurekaConverter(asa_arm)) - context.add_converter(ServiceRegistryConverter(asa_arm)) - context.add_converter(ConfigServerConverter(asa_arm)) - context.add_converter(ACSConverter(asa_arm)) - context.add_converter(LiveViewConverter(asa_arm)) - context.add_converter(ReadMeConverter(asa_arm)) - context.add_converter(ParamConverter(asa_arm)) - context.add_converter(CertConverter(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...") From fbb1081d33143074a885d3f10ab2f08cbef0b623 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Tue, 11 Mar 2025 15:10:38 +0800 Subject: [PATCH 89/98] refine --- src/spring/HISTORY.md | 4 ++++ .../migration/converter/readme_converter.py | 2 +- .../migration/converter/templates/readme.md.j2 | 16 ++++++++-------- src/spring/setup.py | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/spring/HISTORY.md b/src/spring/HISTORY.md index 7c588a7414d..26a944c2639 100644 --- a/src/spring/HISTORY.md +++ b/src/spring/HISTORY.md @@ -1,5 +1,9 @@ Release History =============== +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/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index f17366dbfac..3148594150a 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -38,7 +38,7 @@ def transform_data(): "shouldSystemAssignedIdentityEnabled": should_system_assigned_identity_enabled, "systemAssignedIdentityApps": system_assigned_identity_apps, } - print(f"ReadMeConverter data: {data}") + # print(f"ReadMeConverter data: {data}") return data super().__init__(source, transform_data) diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index 3ac75d82228..eb0b68d0e01 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -113,15 +113,16 @@ You can check the deployment status either in the CLI output or in the Azure Por ## Post-Deployment Configuration Some properties and configurations are not included in the Bicep files and must be manually updated. -{% if data.hasCerts %} +{%- if data.hasCerts %} ### **TLS certificate** -{% if data.shouldSystemAssignedIdentityEnabled %} +{%- 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.keyvault_certs %} - `{{cert.name}}` {%- endfor %} {%- endif %} -{% if data.contentCerts %} + +{%- if data.contentCerts %} Following content type certificates should be manually uploaded to Azure Container Apps environment: {%- for cert in data.contentCerts %} - `{{cert.name}}` @@ -129,7 +130,7 @@ Following content type certificates should be manually uploaded to Azure Contain {%- endif %} {%- endif %} -{% if data.customDomains %} +{%- 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/en-us/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 %} @@ -137,16 +138,15 @@ You have custom domains configured in Azure Spring Apps. You can follow the step {%- endfor %} {%- endif %} -{% if data.system_assigned_identity_apps %} +{%- if data.system_assigned_identity_apps %} ### 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 to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. {%- for app in data.system_assigned_identity_apps %} - `{{app.name}}` {%- endfor %} {%- endif %} -{% if data.greenDeployments %} +{%- 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/en-us/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 %} @@ -154,7 +154,7 @@ Following applications have enabled blue-green deployments in Azure Spring Apps, {%- endfor %} {%- endif %} -{% if data.auto_scale_enabled_deployments %} +{%- if data.auto_scale_enabled_deployments %} ### Auto scale You have enabled auto scaling for following applications in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. {%- for deployment in data.autoScaleEnabledApps %} diff --git a/src/spring/setup.py b/src/spring/setup.py index 46df61892c5..4465bd1746c 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.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 7cbf810d34659a87d6db68de1ea663c691aa1e45 Mon Sep 17 00:00:00 2001 From: ninpan-ms <71061174+ninpan-ms@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:57:43 +0800 Subject: [PATCH 90/98] Update src/spring/azext_spring/migration/converter/environment_converter.py Co-authored-by: Yan Zhu <105691024+yanzhudd@users.noreply.github.com> --- .../azext_spring/migration/converter/environment_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index ced33e1843b..2bc3a4c600e 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -32,7 +32,7 @@ def transform_data(): if asa_zone_redundant is not None: data["zoneRedundant"] = str(asa_zone_redundant).lower() - asa_maintenance_window = asa_service['properties'].get('maintenanceScheduleConfiguration') + asa_maintenance_window = asa_service['properties'].get('maintenanceScheduleConfiguration', None) if asa_maintenance_window: aca_maintenance_window = [{ "weekDay": asa_maintenance_window['day'], From 01ea0b6ba871b1fd105f50dce30e55060c8a4326 Mon Sep 17 00:00:00 2001 From: ninpan-ms <71061174+ninpan-ms@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:58:41 +0800 Subject: [PATCH 91/98] Update src/spring/azext_spring/migration/converter/acs_converter.py Co-authored-by: Yan Zhu <105691024+yanzhudd@users.noreply.github.com> --- src/spring/azext_spring/migration/converter/acs_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 983da34963c..69e6412c207 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -43,7 +43,7 @@ def _get_configurations_and_params(self, acs): configurations = [] params = [] - git_repos = acs.get('properties', {}).get('settings', {}).get('gitProperty', {}).get('repositories') + 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')) From b96ebe1cde85e44ca052978642a95633202d1b0c Mon Sep 17 00:00:00 2001 From: ninpan-ms <71061174+ninpan-ms@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:26:53 +0800 Subject: [PATCH 92/98] Update src/spring/azext_spring/_params.py Co-authored-by: Yan Zhu <105691024+yanzhudd@users.noreply.github.com> --- src/spring/azext_spring/_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/_params.py b/src/spring/azext_spring/_params.py index bdd6f44865c..eafc579e898 100644 --- a/src/spring/azext_spring/_params.py +++ b/src/spring/azext_spring/_params.py @@ -1310,5 +1310,5 @@ def prepare_common_logs_argument(c): with self.argument_context('spring export') as c: c.argument('service', service_name_type) - c.argument('target', help='The target Azure service to migrate to, allowed values: aca, azure-container-apps.') + 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.') From 6876c55b90a7f11f9e8a06d3a7ef017a6a77d229 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Wed, 12 Mar 2025 18:20:13 +0800 Subject: [PATCH 93/98] put autoscale part as static in readme --- .../azext_spring/migration/converter/readme_converter.py | 2 -- .../migration/converter/templates/readme.md.j2 | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index 3148594150a..f4d20b81c53 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -20,7 +20,6 @@ def transform_data(): 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 - auto_scale_enabled_deployments = [] # todo system_assigned_identity_apps = [] # todo data = { @@ -34,7 +33,6 @@ def transform_data(): "keyVaultCerts": keyvault_certs, "contentCerts": content_certs, "greenDeployments": self._transform_deployments(green_deployments), - "autoScaleEnabledApps": auto_scale_enabled_deployments, "shouldSystemAssignedIdentityEnabled": should_system_assigned_identity_enabled, "systemAssignedIdentityApps": system_assigned_identity_apps, } diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index eb0b68d0e01..360f6d3ba55 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -154,13 +154,8 @@ Following applications have enabled blue-green deployments in Azure Spring Apps, {%- endfor %} {%- endif %} -{%- if data.auto_scale_enabled_deployments %} ### Auto scale -You have enabled auto scaling for following applications in Azure Spring Apps, you need to manually configure the different auto scaling rules in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. -{%- for deployment in data.autoScaleEnabledApps %} - - `{{deployment.appName}}/{{deployment.name}}` -{%- endfor %} -{%- endif %} +Because Azure Container Apps leverage Kubernetes Event-driven Autoscaling (KEDA) for auto-scaling, which is different from Azure Spring Apps that use Azure Monitor, the migration tools cannot transfer these settings for you. You will need to manually configure the auto-scaling settings in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/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/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. From 09d7b9c8f560d900019eaa15b5f3db8e27d450c7 Mon Sep 17 00:00:00 2001 From: Guitar Sheng Date: Wed, 12 Mar 2025 18:37:12 +0800 Subject: [PATCH 94/98] fullfill readme to dynamicly generate content for managed identity part --- .../migration/converter/base_converter.py | 6 +++++ .../migration/converter/readme_converter.py | 23 +++++++++++-------- .../converter/templates/readme.md.j2 | 8 +++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index d8c992a20a4..c01b81ac207 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -285,3 +285,9 @@ def get_certificates_by_type(self, type): 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' \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index f4d20b81c53..a99da0332d4 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -10,31 +10,26 @@ class ReadMeConverter(BaseConverter): def __init__(self, source): def transform_data(): - is_vnet = self.wrapper_data.is_vnet() - build_results_deployments = self.wrapper_data.get_build_results_deployments() - container_deployments = self.wrapper_data.get_container_deployments() - is_support_configserver = self.wrapper_data.is_support_ossconfigserver() 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 - system_assigned_identity_apps = [] # todo data = { - "isVnet": is_vnet, - "containerApps": container_deployments, - "buildResultsApps": build_results_deployments, + "isVnet": self.wrapper_data.is_vnet(), + "containerApps": self.wrapper_data.get_container_deployments(), + "buildResultsApps": self.wrapper_data.get_build_results_deployments(), "hasApps": len(apps) > 0, - "isSupportConfigServer": is_support_configserver, + "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": system_assigned_identity_apps, + "systemAssignedIdentityApps": self._get_system_assigned_identity_apps(), } # print(f"ReadMeConverter data: {data}") return data @@ -62,3 +57,11 @@ def _transform_domains(self, domains): } 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 \ 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 index 360f6d3ba55..c9348948029 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -117,7 +117,7 @@ Some properties and configurations are not included in the Bicep files and must ### **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.keyvault_certs %} +{%- for cert in data.keyVaultCerts %} - `{{cert.name}}` {%- endfor %} {%- endif %} @@ -138,10 +138,10 @@ You have custom domains configured in Azure Spring Apps. You can follow the step {%- endfor %} {%- endif %} -{%- if data.system_assigned_identity_apps %} +{%- 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 to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. -{%- for app in data.system_assigned_identity_apps %} +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/en-us/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 %} From 12e850af9289ac6a1d39768b89cc76897a3ed888 Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Mar 2025 14:55:51 +0800 Subject: [PATCH 95/98] Add mismatch messages --- .../migration/converter/acs_converter.py | 11 +++++++ .../migration/converter/app_converter.py | 22 +++++++++----- .../migration/converter/base_converter.py | 6 +++- .../migration/converter/cert_converter.py | 2 +- .../converter/environment_converter.py | 13 +++++++-- .../migration/converter/gateway_converter.py | 29 ++++++++++++++----- .../migration/converter/readme_converter.py | 2 +- .../converter/templates/readme.md.j2 | 1 + 8 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/acs_converter.py b/src/spring/azext_spring/migration/converter/acs_converter.py index 69e6412c207..b2dfdf98261 100644 --- a/src/spring/azext_spring/migration/converter/acs_converter.py +++ b/src/spring/azext_spring/migration/converter/acs_converter.py @@ -2,8 +2,11 @@ # 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): @@ -54,6 +57,7 @@ def _get_configurations_and_params(self, acs): 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] @@ -66,6 +70,7 @@ def _get_configurations_and_params(self, acs): 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 @@ -83,3 +88,9 @@ def _add_secret_config(self, key, value, configurations, params): 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 index 00dbbc5907d..98b995ae6c6 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -23,6 +23,8 @@ def transform_data_item(self, app): blueDeployment = self._transform_deployment(blueDeployment) 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') serviceBinds = self._get_service_bind(app) ingress = self._get_ingress(app, tier) @@ -112,11 +114,11 @@ def _transform_deployment(self, deployment): return { "name": self._get_resource_name(deployment), "env": self._convert_env(env), - "livenessProbe": self._convert_probe(liveness_probe, tier), - "readinessProbe": self._convert_probe(readiness_probe, tier), - "startupProbe": self._convert_probe(startup_probe, tier), + "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) or memorySize, + "memorySize": self._get_memory_by_cpu(cpuCore, memorySize, deployment) or memorySize, "scale": self._convert_scale(scale), "capacity": capacity, } @@ -132,7 +134,7 @@ def _convert_env(self, env): # 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): + def _get_memory_by_cpu(self, cpu, asa_memory_size, deployment): cpu_memory_map = { 0.25: "0.5Gi", 0.5: "1.0Gi", @@ -151,10 +153,15 @@ def _get_memory_by_cpu(self, cpu): 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): + def _convert_probe(self, probe, tier, deployment): if probe is None: return None if probe.get("disableProbe") is True: @@ -164,6 +171,7 @@ def _convert_probe(self, probe, tier): 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) @@ -186,7 +194,7 @@ def _convert_probe(self, probe, tier): result["tcpSocket"] = tcpSocket execAction = self._convert_exec_probe_action(probe, tier) if execAction is not None: - logger.warning(f"Mismatch: The ExecAction {execAction} is not supported in Azure Container Apps.") + 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): diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index c01b81ac207..05bf5ad33c1 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -130,6 +130,10 @@ def _get_mount_options(self, disk_props): # 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 + # module name def _get_app_module_name(self, app): appName = self._get_resource_name(app) @@ -290,4 +294,4 @@ 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' \ No newline at end of file + return identity.get('type') == 'SystemAssigned' diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 7eb37c585f9..97ab00ec07c 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -15,7 +15,7 @@ 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"Mismatch: The content certificate: {cert['name']} cannot be exported automatically. Please export it manually.") + logger.warning(f"Mismatch: The content certificate '{cert['name']}' cannot be exported automatically. Please export it manually.") return self.wrapper_data.get_certificates() super().__init__(source, transform_data) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 2bc3a4c600e..55cbe99a3df 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -2,8 +2,11 @@ # 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): @@ -30,7 +33,11 @@ def transform_data(): asa_zone_redundant = asa_service['properties'].get('zoneRedundant') if asa_zone_redundant is not None: - data["zoneRedundant"] = str(asa_zone_redundant).lower() + if asa_zone_redundant == True and self.wrapper_data.is_vnet() == 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: @@ -56,8 +63,10 @@ def _get_app_storage_configs(self, apps): for app in apps: # Check if app has properties and customPersistentDiskProperties if 'properties' in app and 'customPersistentDisks' in app['properties']: - disks = app['properties']['customPersistentDisks'] + disks = app['properties'].get('customPersistentDisks', []) for disk_props in disks: + if self._get_storage_enable_subpath(disk_props) == 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 = { 'containerAppEnvStorageName': self._get_resource_name_of_storage(app, disk_props), diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index 471ae059eff..ec1b4890b11 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -25,6 +25,7 @@ def transform_data(): 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", @@ -42,10 +43,13 @@ 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(): - configurations.append({ - "propertyName": key, - "value": value, - }) + 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({ @@ -91,16 +95,25 @@ def _get_filters(self, route_name, r): if r.get('filters'): for f in r.get('filters'): if 'cors' in f.lower(): - logger.warning(f"Mismatch: 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.") + 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: - logger.warning("CORS configuration detected, please refer to public doc to migrate CORS feature of gateway for Spring to Azure Container Apps.") + 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(f"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/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index a99da0332d4..b2e42f4f299 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -64,4 +64,4 @@ def _get_system_assigned_identity_apps(self): 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 \ No newline at end of file + return system_assigned_identity_apps diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index c9348948029..947d747406b 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -123,6 +123,7 @@ Following TLS certificates from Azure Key Vault have been migrated by the migrat {%- endif %} {%- if data.contentCerts %} + Following content type certificates should be manually uploaded to Azure Container Apps environment: {%- for cert in data.contentCerts %} - `{{cert.name}}` From 94c65ae6c91f022c3f741eac5ca79ebaebb94f6c Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Mar 2025 15:00:08 +0800 Subject: [PATCH 96/98] default --- .../azext_spring/migration/converter/environment_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 55cbe99a3df..cc60f7e985a 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -31,7 +31,7 @@ def transform_data(): "internal": str(True).lower(), } - asa_zone_redundant = asa_service['properties'].get('zoneRedundant') + asa_zone_redundant = asa_service['properties'].get('zoneRedundant', False) if asa_zone_redundant is not None: if asa_zone_redundant == True and self.wrapper_data.is_vnet() == False: logger.warning("Mismatch: Zone redundant is only supported in VNet environment for Azure Container Apps.") From b4dee674f899270d78caa6ac6d88fe6a109d43eb Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Mar 2025 15:44:41 +0800 Subject: [PATCH 97/98] action --- src/spring/azext_spring/migration/converter/cert_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 97ab00ec07c..49c9add38a7 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -15,7 +15,7 @@ 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"Mismatch: The content certificate '{cert['name']}' cannot be exported automatically. Please export it manually.") + logger.warning(f"Action Needed: The content certificate '{cert['name']}' cannot be exported automatically. Please export it manually.") return self.wrapper_data.get_certificates() super().__init__(source, transform_data) From f4533ecb4ae98a52912535e7c0a51bf0233adb6e Mon Sep 17 00:00:00 2001 From: Ningting Pan Date: Thu, 13 Mar 2025 15:54:19 +0800 Subject: [PATCH 98/98] style --- .../azext_spring/migration/converter/environment_converter.py | 4 ++-- .../azext_spring/migration/converter/gateway_converter.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index cc60f7e985a..7fe1fa84eae 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -33,7 +33,7 @@ def transform_data(): asa_zone_redundant = asa_service['properties'].get('zoneRedundant', False) if asa_zone_redundant is not None: - if asa_zone_redundant == True and self.wrapper_data.is_vnet() == False: + 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: @@ -65,7 +65,7 @@ def _get_app_storage_configs(self, apps): 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) == True: + 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 = { diff --git a/src/spring/azext_spring/migration/converter/gateway_converter.py b/src/spring/azext_spring/migration/converter/gateway_converter.py index ec1b4890b11..38f9acbb95f 100644 --- a/src/spring/azext_spring/migration/converter/gateway_converter.py +++ b/src/spring/azext_spring/migration/converter/gateway_converter.py @@ -113,7 +113,7 @@ def _check_features(self, scg_properties): 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(f"Mismatch: Custom domains of gateway is not supported in Gateway for Spring of Azure Container Apps.") + 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"