Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.

Commit 2126411

Browse files
ninpan-msbavneetsingh16
authored andcommitted
[Spring] Add migration command from Azure Spring Apps to Azure Container Apps (Azure#8538)
1 parent 9c52ad9 commit 2126411

36 files changed

+2461
-4
lines changed

src/spring/HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
Release History
22
===============
3+
1.27.0
4+
---
5+
* 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.
6+
37
1.26.1
48
---
59
* Fix command `az spring app update`, so that it can detect update failure and return error message.

src/spring/azext_spring/_help.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,3 +1809,11 @@
18091809
- name: Clean up private DNS zone with Azure Spring Apps.
18101810
text: az spring private-dns-zone clean --service MyAzureSpringAppsInstance --resource-group MyResourceGroup
18111811
"""
1812+
1813+
helps['spring export'] = """
1814+
type: command
1815+
short-summary: Commands to export target Azure resource definitions from Azure Spring Apps.
1816+
examples:
1817+
- name: Generate corresponding bicep files and README doc to create Azure Container Apps service.
1818+
text: az spring export --target aca --service MyAzureSpringAppsInstance --resource-group MyResourceGroup --output-folder output
1819+
"""

src/spring/azext_spring/_params.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,3 +1307,8 @@ def prepare_common_logs_argument(c):
13071307
c.argument('zone_id', help='The resource id of the private DNS zone which you would like to configure with the service instance.')
13081308
with self.argument_context('spring private-dns-zone clean') as c:
13091309
c.argument('service', service_name_type)
1310+
1311+
with self.argument_context('spring export') as c:
1312+
c.argument('service', service_name_type)
1313+
c.argument('target', arg_type=get_enum_type(["aca", "azure-container-apps"]), help='The target Azure service to migrate to.')
1314+
c.argument('output_folder', help='The output folder for the generated Bicep files.')

src/spring/azext_spring/commands.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
# pylint: disable=line-too-long
77
from azure.cli.core.commands import CliCommandType
8+
from azure.cli.core.profiles import ResourceType
89
from azext_spring._utils import handle_asc_exception
910

1011
from ._client_factory import (cf_spring,
@@ -519,5 +520,9 @@ def load_command_table(self, _):
519520
exception_handler=handle_asc_exception, is_preview=True) as g:
520521
g.custom_command('list', 'job_execution_instance_list', validator=job_validators.validate_job_execution_instance_list)
521522

523+
with self.command_group('spring', custom_command_type=spring_routing_util, resource_type=ResourceType.MGMT_RESOURCE_RESOURCES,
524+
exception_handler=handle_asc_exception, is_preview=True) as g:
525+
g.custom_command('export', 'spring_migration_start')
526+
522527
with self.command_group('spring', exception_handler=handle_asc_exception):
523528
pass
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
from knack.log import get_logger
6+
from .base_converter import BaseConverter
7+
8+
logger = get_logger(__name__)
9+
10+
11+
# Concrete Converter Subclass for Config Server
12+
class ACSConverter(BaseConverter):
13+
14+
CONFIGURATION_KEY_PREFIX = "spring.cloud.config.server.git"
15+
KEY_URI = ".uri"
16+
KEY_LABEL = ".default-label"
17+
KEY_SEARCH_PATHS = ".search-paths"
18+
KEY_USERNAME = ".username"
19+
KEY_PASSWORD = ".password"
20+
KEY_PRIVATE_KEY = ".private-key"
21+
KEY_HOST_KEY = ".host-key"
22+
KEY_HOST_KEY_ALGORITHM = ".host-key-algorithm"
23+
KEY_PATTERN = ".pattern"
24+
25+
def __init__(self, source):
26+
def transform_data():
27+
if self.wrapper_data.is_support_ossconfigserver() is False and self.wrapper_data.is_support_acs():
28+
acs = self.wrapper_data.get_resources_by_type('Microsoft.AppPlatform/Spring/configurationServices')[0]
29+
name = "config"
30+
configurations, params = self._get_configurations_and_params(acs)
31+
replicas = 2
32+
return {
33+
"configServerName": name,
34+
"params": params,
35+
"configurations": configurations,
36+
"replicas": replicas
37+
}
38+
else:
39+
return None
40+
super().__init__(source, transform_data)
41+
42+
def get_template_name(self):
43+
return "config_server.bicep"
44+
45+
def _get_configurations_and_params(self, acs):
46+
configurations = []
47+
params = []
48+
49+
git_repos = acs.get('properties', {}).get('settings', {}).get('gitProperty', {}).get('repositories', None)
50+
if git_repos is not None and len(git_repos) > 0:
51+
default_repo = git_repos[0]
52+
self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_URI, default_repo.get('uri'))
53+
self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_LABEL, default_repo.get('label'))
54+
self._add_property_if_exists(configurations, self.CONFIGURATION_KEY_PREFIX + self.KEY_SEARCH_PATHS, default_repo.get('searchPaths'))
55+
self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_USERNAME, default_repo.get('username'), configurations, params)
56+
self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_PASSWORD, default_repo.get('password'), configurations, params)
57+
self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_PRIVATE_KEY, default_repo.get('privateKey'), configurations, params)
58+
self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY, default_repo.get('hostKey'), configurations, params)
59+
self._add_secret_config(self.CONFIGURATION_KEY_PREFIX + self.KEY_HOST_KEY_ALGORITHM, default_repo.get('hostKeyAlgorithm'), configurations, params)
60+
self._check_patterns(default_repo)
61+
62+
for i in range(1, len(git_repos)):
63+
repo = git_repos[i]
64+
configuration_key_repo_prefix = self.CONFIGURATION_KEY_PREFIX + ".repos." + repo['name']
65+
self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_URI, repo.get('uri'))
66+
self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_LABEL, repo.get('label'))
67+
self._add_property_if_exists(configurations, configuration_key_repo_prefix + self.KEY_SEARCH_PATHS, repo.get('searchPaths'))
68+
self._add_secret_config(configuration_key_repo_prefix + self.KEY_USERNAME, repo.get('username'), configurations, params)
69+
self._add_secret_config(configuration_key_repo_prefix + self.KEY_PASSWORD, repo.get('password'), configurations, params)
70+
self._add_secret_config(configuration_key_repo_prefix + self.KEY_PRIVATE_KEY, repo.get('privateKey'), configurations, params)
71+
self._add_secret_config(configuration_key_repo_prefix + self.KEY_HOST_KEY, repo.get('hostKey'), configurations, params)
72+
self._add_secret_config(configuration_key_repo_prefix + self.KEY_HOST_KEY_ALGORITHM, repo.get('hostKeyAlgorithm'), configurations, params)
73+
self._check_patterns(repo)
74+
75+
return configurations, params
76+
77+
def _add_property_if_exists(self, configurations, key, value):
78+
if value:
79+
if isinstance(value, (list, tuple)):
80+
value = ",".join(map(str, value))
81+
configurations.append({
82+
"propertyName": key,
83+
"value": value
84+
})
85+
86+
def _add_secret_config(self, key, value, configurations, params):
87+
if value:
88+
param_name = key.replace(".", "_").replace("-", "_")
89+
self._add_property_if_exists(configurations, key, param_name)
90+
params.append(param_name)
91+
92+
def _check_patterns(self, repo):
93+
patterns = repo.get('patterns', [])
94+
if len(patterns) > 0:
95+
pattern_str = ",".join(map(str, patterns))
96+
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.")

0 commit comments

Comments
 (0)