Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/spring/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Release History
===============
1.28.2
---
* Remove DATA_COSMOS_TABLE and DATA_STORAGE references

1.28.1
---
* Fix clean up config file patterns of Application Configuration Service.
Expand Down
2 changes: 1 addition & 1 deletion src/spring/azext_spring/_breaking_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
from azure.cli.core.breaking_change import register_command_group_deprecate

# https://aka.ms/asaretirement
register_command_group_deprecate('spring', target_version='Mar 2028', hide=True)
register_command_group_deprecate('spring', target_version='Mar 2028', hide=True)
2 changes: 1 addition & 1 deletion src/spring/azext_spring/_deployment_deployable_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def get_logs_loop():
sleep(10)

logger.warning("Trying to fetch build logs")
stream_logs(client.deployments, resource_group, service,
stream_logs(self.cmd, client.deployments, resource_group, service,
app, deployment, logger_level_func=print)
old_log_url = get_log_url()
timer = Timer(3, get_logs_loop)
Expand Down
8 changes: 5 additions & 3 deletions src/spring/azext_spring/_deployment_uploadable_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(self, upload_url, cli_ctx):
self.relative_name = relative_name
self.sas_token = sas_token
self.cli_ctx = cli_ctx
self.upload_url = upload_url

def upload_and_build(self, artifact_path, **_):
if not artifact_path:
Expand All @@ -41,9 +42,10 @@ def upload_and_build(self, artifact_path, **_):
raise InvalidArgumentValueError('Unexpected artifact file type, must be one of .zip, .tar.gz, .tar, .jar, .war.')

def _upload(self, artifact_path):
FileService = get_sdk(self.cli_ctx, ResourceType.DATA_STORAGE, 'file#FileService')
file_service = FileService(self.account_name, sas_token=self.sas_token, endpoint_suffix=self.endpoint_suffix)
file_service.create_file_from_path(self.share_name, None, self.relative_name, artifact_path)
ShareFileClient = get_sdk(self.cli_ctx, ResourceType.DATA_STORAGE_FILESHARE, '_file_client#ShareFileClient')
file_client = ShareFileClient.from_file_url(self.upload_url)
with open(artifact_path, 'rb') as stream:
file_client.upload_file(data=stream)


class FolderUpload(FileUpload):
Expand Down
39 changes: 12 additions & 27 deletions src/spring/azext_spring/_stream_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
from knack.util import CLIError
from knack.log import get_logger
from azure.core.exceptions import HttpResponseError
from azure.multiapi.storage.v2018_11_09.blob import AppendBlobService
from azure.cli.core.profiles import ResourceType, get_sdk
from azure.common import AzureHttpError
from ._utils import get_blob_info

logger = get_logger(__name__)

DEFAULT_CHUNK_SIZE = 1024 * 4
DEFAULT_LOG_TIMEOUT_IN_SEC = 60 * 30 # 30 minutes


def stream_logs(client,
def stream_logs(cmd,
client,
resource_group,
service,
app,
Expand All @@ -47,18 +47,13 @@ def stream_logs(client,
logger.warning("%s Empty SAS URL.", error_msg)
raise CLIError(error_msg)

account_name, endpoint_suffix, container_name, blob_name, sas_token = get_blob_info(
log_file_sas)
BlobClient = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE_BLOB, '_blob_client#BlobClient')
blob_client = BlobClient.from_blob_url(log_file_sas)

_stream_logs(no_format,
DEFAULT_CHUNK_SIZE,
DEFAULT_LOG_TIMEOUT_IN_SEC,
AppendBlobService(
account_name=account_name,
sas_token=sas_token,
endpoint_suffix=endpoint_suffix),
container_name,
blob_name,
blob_client,
raise_error_on_failure,
logger_level_func)

Expand All @@ -67,8 +62,6 @@ def _stream_logs(no_format, # pylint: disable=too-many-locals, too-many-stateme
byte_size,
timeout_in_seconds,
blob_service,
container_name,
blob_name,
raise_error_on_failure,
logger_level_func):

Comment on lines 66 to 67
Copy link

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function signature has changed but the docstring and parameter documentation are missing. The removal of container_name and blob_name parameters should be documented, and the new blob_service parameter (now a BlobClient) should be clearly documented.

Suggested change
logger_level_func):
logger_level_func):
"""
Streams logs from an Azure Blob storage using the provided BlobClient.
Note: The function signature has changed. The parameters `container_name` and `blob_name` have been removed.
Instead, the `blob_service` parameter should be provided as an instance of BlobClient.
Args:
no_format (bool): If True, disables color formatting of logs.
byte_size (int): The chunk size in bytes to read from the blob.
timeout_in_seconds (int): The maximum time in seconds to stream logs before timing out.
blob_service (BlobClient): The Azure BlobClient instance pointing to the log blob.
raise_error_on_failure (bool): If True, raises an error when log streaming fails.
logger_level_func (function): The logger function to use for warnings and errors.
"""

Copilot uses AI. Check for mistakes.
Expand All @@ -78,7 +71,6 @@ def _stream_logs(no_format, # pylint: disable=too-many-locals, too-many-stateme
stream = BytesIO()
metadata = {}
start = 0
end = byte_size - 1
available = 0
sleep_time = 1
max_sleep_time = 15
Expand All @@ -97,11 +89,9 @@ def safe_get_blob_properties():
'''
nonlocal blob_exists
if not blob_exists:
blob_exists = blob_service.exists(
container_name=container_name, blob_name=blob_name)
blob_exists = blob_service.exists()
if blob_exists:
return blob_service.get_blob_properties(
container_name=container_name, blob_name=blob_name)
return blob_service.get_blob_properties()
return None

# Try to get the initial properties so there's no waiting.
Expand All @@ -110,7 +100,7 @@ def safe_get_blob_properties():
props = safe_get_blob_properties()
if props:
metadata = props.metadata
available = props.properties.content_length
available = props.size
except (AttributeError, AzureHttpError):
pass

Expand All @@ -123,18 +113,13 @@ def safe_get_blob_properties():

try:
old_byte_size = len(stream.getvalue())
blob_service.get_blob_to_stream(
container_name=container_name,
blob_name=blob_name,
start_range=start,
end_range=end,
stream=stream)
downloader = blob_service.download_blob(offset=start, length=byte_size, max_concurrency=1)
downloader.readinto(stream)
Comment on lines +116 to +117
Copy link

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readinto method may not behave as expected with BytesIO streams. The modern BlobClient API typically returns a StorageStreamDownloader that should use readall() or iterate over chunks. Consider using stream.write(downloader.readall()) instead.

Suggested change
downloader = blob_service.download_blob(offset=start, length=byte_size, max_concurrency=1)
downloader.readinto(stream)
stream.write(downloader.readall())

Copilot uses AI. Check for mistakes.

curr_bytes = stream.getvalue()
new_byte_size = len(curr_bytes)
amount_read = new_byte_size - old_byte_size
start += amount_read
end = start + byte_size - 1

# Only scan what's newly read. If nothing is read, default to 0.
min_scan_range = max(new_byte_size - amount_read - 1, 0)
Expand Down Expand Up @@ -165,7 +150,7 @@ def safe_get_blob_properties():
props = safe_get_blob_properties()
if props:
metadata = props.metadata
available = props.properties.content_length
available = props.size
except AzureHttpError as ae:
if ae.status_code != 404:
raise CLIError(ae)
Expand Down
2 changes: 1 addition & 1 deletion src/spring/azext_spring/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def parse_auth_flags(auth_list):
def app_get_build_log(cmd, client, resource_group, service, name, deployment=None):
if deployment.properties.source.type != "Source":
raise CLIError("{} deployment has no build logs.".format(deployment.properties.source.type))
return stream_logs(client.deployments, resource_group, service, name, deployment.name)
return stream_logs(cmd, client.deployments, resource_group, service, name, deployment.name)


def app_tail_log(cmd, client, resource_group, service, name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interactions:
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
uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/exportTemplate?api-version=2024-11-01
response:
body:
string: ''
Expand Down
9 changes: 7 additions & 2 deletions src/spring/azext_spring/tests/latest/test_asa_app_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import tempfile

from knack.util import CLIError
from azure.cli.testsdk import (ScenarioTest, record_only)
from azure.cli.testsdk import (ScenarioTest, record_only, live_only)
from .custom_preparers import (SpringPreparer, SpringResourceGroupPreparer, SpringAppNamePreparer)
from .custom_recording_processor import (SpringTestEndpointReplacer)
from .custom_dev_setting_constant import SpringTestEnvironmentEnum
Expand All @@ -31,6 +31,7 @@ def test_replacer(self):
actual_string = SpringTestEndpointReplacer()._replace(original_string)
self.assertEqual(expected_string, actual_string)

@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.STANDARD['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.STANDARD['spring'])
@SpringAppNamePreparer()
Expand Down Expand Up @@ -58,6 +59,7 @@ def test_deploy_app(self, resource_group, spring, app):
with self.assertRaisesRegex(CLIError, "112404: Exit code 1: application error"):
self.cmd('spring app deploy -n {app} -g {rg} -s {serviceName} --artifact-path {file} --version v1')

@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.STANDARD['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.STANDARD['spring'])
@SpringAppNamePreparer()
Expand Down Expand Up @@ -141,6 +143,7 @@ def test_deploy_app_3(self, resource_group, spring, app):


class AppCRUD(ScenarioTest):
@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.STANDARD['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.STANDARD['spring'])
@SpringAppNamePreparer()
Expand Down Expand Up @@ -241,6 +244,7 @@ def test_app_create_binding_tanzu_components(self, resource_group, spring, app):
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.ENTERPRISE['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.ENTERPRISE['spring'], location = 'eastasia')
@SpringAppNamePreparer()
@live_only()
def test_enterprise_app_crud(self, resource_group, spring, app):
self.kwargs.update({
'app': app,
Expand Down Expand Up @@ -332,7 +336,7 @@ def test_blue_green_deployment(self, resource_group, spring, app):

@record_only()
class CustomImageTest(ScenarioTest):

@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.STANDARD['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.STANDARD['spring'])
@SpringAppNamePreparer()
Expand All @@ -352,6 +356,7 @@ def test_app_deploy_container(self, resource_group, spring, app):
self.check('properties.source.customContainer.languageFramework', None),
])

@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.STANDARD['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.STANDARD['spring'])
@SpringAppNamePreparer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
# --------------------------------------------------------------------------------------------
import json
import unittest
from azure.cli.testsdk import (ScenarioTest)
import time
from azure.cli.testsdk import (ScenarioTest, live_only)
from .common.test_utils import get_test_cmd
from .custom_preparers import (SpringPreparer, SpringResourceGroupPreparer)
from .custom_dev_setting_constant import SpringTestEnvironmentEnum
Expand Down Expand Up @@ -127,7 +128,7 @@ def test_asa_acc_delete_configure_dev_tool_portal_wait(self):


class ApiApplicationAcceleratorTest(ScenarioTest):

@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.ENTERPRISE['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.ENTERPRISE['spring'])
def test_application_accelerator(self, resource_group, spring):
Expand Down Expand Up @@ -157,6 +158,7 @@ def test_application_accelerator(self, resource_group, spring):

self.cmd('spring application-accelerator delete --yes -g {rg} -s {serviceName}')

time.sleep(10)
self.cmd('spring dev-tool show -g {rg} -s {serviceName}', checks=[
self.check('properties.features.applicationAccelerator.state', 'Disabled')
])
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
# --------------------------------------------------------------------------------------------
import json
import unittest
from azure.cli.testsdk import (ScenarioTest)
import time
from azure.cli.testsdk import (ScenarioTest, live_only)
from .common.test_utils import get_test_cmd
from .custom_preparers import SpringPreparer, SpringResourceGroupPreparer
from .custom_dev_setting_constant import SpringTestEnvironmentEnum
Expand Down Expand Up @@ -128,7 +129,7 @@ def test_asa_alv_delete_configure_dev_tool_portal_wait(self):


class LiveViewTest(ScenarioTest):

@live_only()
@SpringResourceGroupPreparer(dev_setting_name=SpringTestEnvironmentEnum.ENTERPRISE['resource_group_name'])
@SpringPreparer(**SpringTestEnvironmentEnum.ENTERPRISE['spring'])
def test_live_view(self, resource_group, spring):
Expand All @@ -152,6 +153,7 @@ def test_live_view(self, resource_group, spring):

self.cmd('spring application-live-view delete -g {rg} -s {serviceName} -y')

time.sleep(10)
self.cmd('spring dev-tool show -g {rg} -s {serviceName}', checks=[
self.check('properties.features.applicationLiveView.state', 'Disabled')
])
2 changes: 1 addition & 1 deletion src/spring/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

# TODO: Confirm this is the right version number you want and it matches your
# HISTORY.rst entry.
VERSION = '1.28.1'
VERSION = '1.28.2'

# The full list of classifiers is available at
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
Expand Down
Loading