Skip to content
Open
145 changes: 122 additions & 23 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -6152,12 +6152,16 @@ def _get_raw_stacks_from_api(self):
return list(self._client.provider.get_web_app_stacks(stack_os_type=None))

def _parse_raw_stacks(self, stacks):
# Track seen runtime display names to avoid duplicates in Linux parsing.
# Linux Java containers (e.g., JBOSSEAP) can produce duplicate entries across major versions.
# Windows parsing doesn't have this issue due to its different structure.
seen_runtimes = set()
for lang in stacks:
for major_version in lang.major_versions:
if self._linux:
if lang.display_text.lower() == "java":
continue
self._parse_major_version_linux(major_version, self._stacks)
self._parse_major_version_linux(major_version, self._stacks, seen_runtimes)
if self._windows:
self._parse_major_version_windows(major_version, self._stacks, self.windows_config_mappings)

Expand Down Expand Up @@ -6302,17 +6306,116 @@ def _filter(minor_version):
return cls._is_valid_runtime_setting(cls._get_runtime_setting(minor_version, linux, java))
return [m for m in major_version.minor_versions if _filter(m)]

@staticmethod
def _java_version_sort_key(version):
"""Sort key for Java versions. Handles formats like "25", "1.8", "11.0", etc.
Returns a negative integer representing the version, so sorted() produces descending order (newest first)."""
if version == "1.8":
return -8 # Treat 1.8 as Java 8
if version.startswith("1."):
# Handle legacy "1.x" format (e.g., "1.7", "1.9")
try:
return -int(version.split('.')[1])
except (IndexError, ValueError):
return 0
# Handle "X.Y" format (e.g., "11.0", "17.0") or plain integers ("25", "21")
try:
return -int(version.split('.')[0])
except ValueError:
return 0

@staticmethod
def _get_java_versions_from_minor_versions(minor_versions):
"""Dynamically extract unique Java versions from minor version values.
Used for Linux Java SE containers where minor.value is like "25.0.0", "21.0.0".
Returns versions sorted in descending order (newest first)."""
java_versions = set()
for minor in minor_versions:
# minor.value is like "25.0.0", "21.0.0", "17.0.0", "11.0.0", "8.0.0" or "1.8.0"
value = minor.value
if value:
# Handle both "1.8" format and newer "25", "21" formats
if value.startswith("1.8"):
java_versions.add("1.8")
else:
# Extract major version number (e.g., "25" from "25.0.0")
major_ver = value.split('.')[0]
if major_ver.isdigit():
java_versions.add(major_ver)
# Sort descending (newest versions first)
return sorted(java_versions, key=_StackRuntimeHelper._java_version_sort_key)

@staticmethod
def _get_java_versions_from_windows_container(container_settings):
"""Dynamically extract Java versions from Windows container settings.
Looks at the 'runtimes' array in additional_properties.
Returns versions sorted in descending order (newest first)."""
java_versions = set()
additional_props = getattr(container_settings, 'additional_properties', {}) or {}
runtimes_array = additional_props.get('runtimes', [])

for runtime_info in runtimes_array:
version = runtime_info.get('runtimeVersion')
if version:
# Add version as-is (e.g., "25", "21", "17", "11", "1.8")
java_versions.add(version)

# Sort descending (newest versions first)
return sorted(java_versions, key=_StackRuntimeHelper._java_version_sort_key)

@staticmethod
def _get_java_runtimes_from_container_settings(container_settings):
"""Dynamically extract Java runtimes from container settings.
Prefers the 'runtimes' array from the API when available (most future-proof),
falls back to individual java*Runtime properties in additional_properties,
and finally SDK-defined properties (java8_runtime, java11_runtime).
Returns list of tuples: (runtime_name, version, is_auto_update)"""
runtimes = []
is_auto_update = getattr(container_settings, 'is_auto_update', False)
additional_props = getattr(container_settings, 'additional_properties', {}) or {}

# Prefer the 'runtimes' array if available (cleanest, most future-proof)
runtimes_array = additional_props.get('runtimes', [])
if runtimes_array:
for runtime_info in runtimes_array:
runtime_name = runtime_info.get('runtime')
version = runtime_info.get('runtimeVersion')
if runtime_name and version:
runtimes.append((runtime_name, version, is_auto_update))
else:
# Fallback: Get runtimes from additional_properties (java*Runtime keys)
for key, value in additional_props.items():
# Match pattern like "java25Runtime", "java21Runtime", etc.
match = re.match(r'^java(\d+)Runtime$', key)
if match and value:
version = match.group(1)
runtimes.append((value, version, is_auto_update))

# Also get runtimes from SDK-defined properties (java8_runtime, java11_runtime)
if getattr(container_settings, 'java11_runtime', None):
# Avoid duplicates if already found in additional_properties
if not any(v == "11" for _, v, _ in runtimes):
runtimes.append((container_settings.java11_runtime, "11", is_auto_update))
if getattr(container_settings, 'java8_runtime', None):
if not any(v == "8" for _, v, _ in runtimes):
runtimes.append((container_settings.java8_runtime, "8", is_auto_update))

# Sort by version descending (newest first)
runtimes.sort(key=lambda x: _StackRuntimeHelper._java_version_sort_key(x[1]))
return runtimes

def _parse_major_version_windows(self, major_version, parsed_results, config_mappings):
java_container_minor_versions = self._get_valid_minor_versions(major_version, linux=False, java=True)
if java_container_minor_versions:
javas = ["21", "17", "11", "1.8"]
if len(java_container_minor_versions) > 0:
leng = len(java_container_minor_versions) if len(java_container_minor_versions) < 3 else 3
java_container_minor_versions = java_container_minor_versions[:leng]
for container in java_container_minor_versions:
container_settings = container.stack_settings.windows_container_settings
java_container = container_settings.java_container
container_version = container_settings.java_container_version
# Get Java versions from the container's runtimes array
javas = self._get_java_versions_from_windows_container(container_settings)
if not javas:
logger.debug("No Java versions found in Windows container settings for "
"container '%s' (version: '%s')", java_container, container_version)
for java in javas:
runtime = self.get_windows_java_runtime(
java,
Expand All @@ -6322,10 +6425,6 @@ def _parse_major_version_windows(self, major_version, parsed_results, config_map
parsed_results.append(runtime)
else:
minor_versions = self._get_valid_minor_versions(major_version, linux=False, java=False)
if "Java" in major_version.display_text:
if len(minor_versions) > 0:
leng = len(minor_versions) if len(minor_versions) < 3 else 3
minor_versions = minor_versions[1:leng]
for minor_version in minor_versions:
settings = minor_version.stack_settings.windows_runtime_settings
if "Java" not in minor_version.display_text:
Expand Down Expand Up @@ -6380,28 +6479,28 @@ def get_windows_java_runtime(self, java_version=None,
linux=False,
is_auto_update=is_auto_update)

def _parse_major_version_linux(self, major_version, parsed_results):
def _parse_major_version_linux(self, major_version, parsed_results, seen_runtimes):
minor_java_container_versions = self._get_valid_minor_versions(major_version, linux=True, java=True)
if "SE" in major_version.display_text:
se_containers = [minor_java_container_versions[0]]
for java in ["21", "17", "11", "1.8"]:
# Dynamically get Java versions from the available minor versions
java_versions = self._get_java_versions_from_minor_versions(minor_java_container_versions)
se_containers = [minor_java_container_versions[0]] if minor_java_container_versions else []
for java in java_versions:
se_java_containers = [c for c in minor_java_container_versions if c.value.startswith(java)]
se_containers = se_containers + se_java_containers[:len(se_java_containers) if len(se_java_containers) < 2 else 2] # pylint: disable=line-too-long
se_containers = se_containers + se_java_containers
minor_java_container_versions = se_containers
if minor_java_container_versions:
leng = len(minor_java_container_versions) if \
len(minor_java_container_versions) < 3 else 3 if \
"SE" not in major_version.display_text else len(minor_java_container_versions)
for minor in minor_java_container_versions[:leng]:
for minor in minor_java_container_versions:
linux_container_settings = minor.stack_settings.linux_container_settings
runtimes = [
(linux_container_settings.additional_properties.get("java21Runtime"), "21", linux_container_settings.is_auto_update), # pylint: disable=line-too-long
(linux_container_settings.additional_properties.get("java17Runtime"), "17", linux_container_settings.is_auto_update), # pylint: disable=line-too-long
(linux_container_settings.java11_runtime, "11", linux_container_settings.is_auto_update),
(linux_container_settings.java8_runtime, "8", linux_container_settings.is_auto_update)]
# Remove the JBoss'_byol' entries from the output
# Dynamically get all Java runtimes from container settings
runtimes = self._get_java_runtimes_from_container_settings(linux_container_settings)
# Remove the 'JBoss _byol' entries from the output
runtimes = [(r, v, au) for (r, v, au) in runtimes if r is not None and not r.endswith("_byol")] # pylint: disable=line-too-long
for runtime_name, version, auto_update in [(r, v, au) for (r, v, au) in runtimes if r is not None]:
# Skip duplicates
if runtime_name in seen_runtimes:
continue
seen_runtimes.add(runtime_name)
runtime = self.Runtime(display_name=runtime_name,
configs={"linux_fx_version": runtime_name},
github_actions_properties={"github_actions_version": version},
Expand Down