Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

{ACR} Add connection pooling with ACR registries. #30520

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open

Conversation

oxpa
Copy link

@oxpa oxpa commented Dec 15, 2024

Related command
az acr repository list

Description
I have a registry of 60000 (sixty thousands) images. Listing them usually takes ~10 minutes on my internet connection. Largely because listing happens in pages by 100 entries per page. So az cli does ~600 requests to list all images. Each request uses it's own connection.
With this change a request to the same registry will reuse an existing connection (if any) or open a new one.
Connection pooling cuts time from 10+ minutes for listing to under a minute (for my specific case). So at least 60 times better.
It should not break things as I make no initial configuration for a session so no parameters are reused. But I'm happy to fix my code if this naïve approach doesn't work.

Testing Guide
az acr repository list --debug --name some_very_large_repository
Before the patch, every request to a registry would produce a message like this:

urllib3.connectionpool: Starting new HTTPS connection ......

With the patch the message goes away, only the first request creates a connection.
The message still there for authentication and other requests.

This checklist is used to make sure that common guidelines for a pull request are followed.

Copy link

azure-client-tools-bot-prd bot commented Dec 15, 2024

❌AzureCLI-FullTest
❌acr
❌2020-09-01-hybrid
❌3.12
Type Test Case Error Message Line
Failed test_helm_delete self = <azure.cli.command_modules.acr.tests.hybrid_2020_09_01.test_acr_commands_mock.AcrMockCommandsTests testMethod=test_helm_delete>
mock_requests_get = <function request at 0x7f13f4a5cb80>
mock_get_access_credentials = <function get_access_credentials at 0x7f13f4a5d1c0>

    @mock.patch('azure.cli.command_modules.acr.helm.get_access_credentials', autospec=True)
    @mock.patch('requests.request', autospec=True)
    def test_helm_delete(self, mock_requests_get, mock_get_access_credentials):
        cmd = self.setup_cmd()
    
        response = mock.MagicMock()
        response.headers = {}
        response.status_code = 200
        mock_requests_get.return_value = response
    
        mock_get_access_credentials.return_value = 'testregistry.azurecr.io', EMPTY_GUID, 'password'
    
        # Delete all versions of a chart
>       acr_helm_delete(cmd, 'testregistry', 'mychart1', repository='testrepository', yes=True)

src/azure-cli/azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:609: 
                                        
src/azure-cli/azure/cli/command_modules/acr/helm.py:103: in acr_helm_delete
    return request_data_from_registry(
                                       _ 

http_method = 'delete', login_server = 'testregistry.azurecr.io'
path = '/helm/v1/testrepository/_charts/mychart1'
username = '00000000-0000-0000-0000-000000000000', password = 'password'
result_index = None, json_payload = None, file_payload = None, params = None
manifest_headers = False, raw = False, retry_times = 3, retry_interval = 5
timeout = 300

    def request_data_from_registry(http_method,
                                   login_server,
                                   path,
                                   username,
                                   password,
                                   result_index=None,
                                   json_payload=None,
                                   file_payload=None,
                                   params=None,
                                   manifest_headers=False,
                                   raw=False,
                                   retry_times=3,
                                   retry_interval=5,
                                   timeout=300):
        if http_method not in ALLOWED_HTTP_METHOD:
            raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD))
    
        if json_payload and file_payload:
            raise ValueError("One of json_payload and file_payload can be specified.")
    
        if http_method in ['get', 'delete'] and (json_payload or file_payload):
            raise ValueError("Empty payload is required for http method: {}".format(http_method))
    
        if http_method in ['patch', 'put'] and not (json_payload or file_payload):
            raise ValueError("Non-empty payload is required for http method: {}".format(http_method))
    
        url = 'https://{}{}'.format(login_server, path)
    
        if manifest_headers:
            headers = get_manifest_authorization_header(username, password)
        else:
            headers = get_authorization_header(username, password)
    
        for i in range(0, retry_times):
            errorMessage = None
            try:
                if file_payload:
                    with open(file_payload, 'rb') as data_payload:
                        logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                        response = session.request(
                            method=http_method,
                            url=url,
                            headers=headers,
                            params=params,
                            data=data_payload,
                            timeout=timeout,
                            verify=(not should_disable_connection_verify())
                        )
                else:
                    logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                    response = session.request(
                        method=http_method,
                        url=url,
                        headers=headers,
                        params=params,
                        json=json_payload,
                        timeout=timeout,
                        verify=(not should_disable_connection_verify())
                    )
    
                log_registry_response(response)
    
                if manifest_headers and raw and response.status_code == 200:
                    return response.content.decode('utf-8'), None, response.status_code
                if response.status_code == 200:
                    result = response.json()[result_index] if result_index else response.json()
                    next_link = response.headers['link'] if 'link' in response.headers else None
                    return result, next_link, response.status_code
                if response.status_code == 201 or response.status_code == 202:
                    result = None
                    try:
                        result = response.json()[result_index] if result_index else response.json()
                    except ValueError as e:
                        logger.debug('Response is empty or is not a valid json. Exception: %s', str(e))
                    return result, None, response.status_code
                if response.status_code == 204:
                    return None, None, response.status_code
                if response.status_code == 401:
>                   raise RegistryException(
                        parse_error_message('Authentication required.', response),
                        response.status_code)
E                       azure.cli.command_modules.acr._docker_utils.RegistryException: 2025-01-26 02:33:48.305521 Error: authentication required, visit https://aka.ms/acr/authorization&nbsp;for&nbsp;more&nbsp;information.&nbsp;Correlation&nbsp;ID:&nbsp;ca05429e-c9b1-4cd1-81fa-35df8aca0ecc.

src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py:637: RegistryException
azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:595
Failed test_helm_list self = <azure.cli.command_modules.acr.tests.hybrid_2020_09_01.test_acr_commands_mock.AcrMockCommandsTests testMethod=test_helm_list>
mock_requests_get = <function request at 0x7f13f4a5ce00>
mock_get_access_credentials = <function get_access_credentials at 0x7f13f4a5d440>

    @mock.patch('azure.cli.command_modules.acr.helm.get_access_credentials', autospec=True)
    @mock.patch('requests.request', autospec=True)
    def test_helm_list(self, mock_requests_get, mock_get_access_credentials):
        cmd = self.setup_cmd()
    
        response = mock.MagicMock()
        response.headers = {}
        response.status_code = 200
        response.content = json.dumps({
            'mychart1': [
                {
                    'name': 'mychart1',
                    'version': '0.2.1'
                },
                {
                    'name': 'mychart1',
                    'version': '0.1.2'
                }
            ],
            'mychart2': [
                {
                    'name': 'mychart2',
                    'version': '2.1.0'
                }
            ]}).encode()
        mock_requests_get.return_value = response
    
        mock_get_access_credentials.return_value = 'testregistry.azurecr.io', EMPTY_GUID, 'password'
>       acr_helm_list(cmd, 'testregistry', repository='testrepository')

src/azure-cli/azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:541: 
                                        
src/azure-cli/azure/cli/command_modules/acr/helm.py:42: in acr_helm_list
    return request_data_from_registry(
                                       _ 

http_method = 'get', login_server = 'testregistry.azurecr.io'
path = '/helm/v1/testrepository/_charts'
username = '00000000-0000-0000-0000-000000000000', password = 'password'
result_index = None, json_payload = None, file_payload = None, params = None
manifest_headers = False, raw = False, retry_times = 3, retry_interval = 5
timeout = 300

    def request_data_from_registry(http_method,
                                   login_server,
                                   path,
                                   username,
                                   password,
                                   result_index=None,
                                   json_payload=None,
                                   file_payload=None,
                                   params=None,
                                   manifest_headers=False,
                                   raw=False,
                                   retry_times=3,
                                   retry_interval=5,
                                   timeout=300):
        if http_method not in ALLOWED_HTTP_METHOD:
            raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD))
    
        if json_payload and file_payload:
            raise ValueError("One of json_payload and file_payload can be specified.")
    
        if http_method in ['get', 'delete'] and (json_payload or file_payload):
            raise ValueError("Empty payload is required for http method: {}".format(http_method))
    
        if http_method in ['patch', 'put'] and not (json_payload or file_payload):
            raise ValueError("Non-empty payload is required for http method: {}".format(http_method))
    
        url = 'https://{}{}'.format(login_server, path)
    
        if manifest_headers:
            headers = get_manifest_authorization_header(username, password)
        else:
            headers = get_authorization_header(username, password)
    
        for i in range(0, retry_times):
            errorMessage = None
            try:
                if file_payload:
                    with open(file_payload, 'rb') as data_payload:
                        logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                        response = session.request(
                            method=http_method,
                            url=url,
                            headers=headers,
                            params=params,
                            data=data_payload,
                            timeout=timeout,
                            verify=(not should_disable_connection_verify())
                        )
                else:
                    logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                    response = session.request(
                        method=http_method,
                        url=url,
                        headers=headers,
                        params=params,
                        json=json_payload,
                        timeout=timeout,
                        verify=(not should_disable_connection_verify())
                    )
    
                log_registry_response(response)
    
                if manifest_headers and raw and response.status_code == 200:
                    return response.content.decode('utf-8'), None, response.status_code
                if response.status_code == 200:
                    result = response.json()[result_index] if result_index else response.json()
                    next_link = response.headers['link'] if 'link' in response.headers else None
                    return result, next_link, response.status_code
                if response.status_code == 201 or response.status_code == 202:
                    result = None
                    try:
                        result = response.json()[result_index] if result_index else response.json()
                    except ValueError as e:
                        logger.debug('Response is empty or is not a valid json. Exception: %s', str(e))
                    return result, None, response.status_code
                if response.status_code == 204:
                    return None, None, response.status_code
                if response.status_code == 401:
>                   raise RegistryException(
                        parse_error_message('Authentication required.', response),
                        response.status_code)
E                       azure.cli.command_modules.acr._docker_utils.RegistryException: 2025-01-26 02:33:48.749787 Error: authentication required, visit https://aka.ms/acr/authorization&nbsp;for&nbsp;more&nbsp;information.&nbsp;Correlation&nbsp;ID:&nbsp;fd3743f9-07f7-4574-9a4d-a65f85d3093a.

src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py:637: RegistryException
azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:512
Failed test_helm_push self = <azure.cli.command_modules.acr.tests.hybrid_2020_09_01.test_acr_commands_mock.AcrMockCommandsTests testMethod=test_helm_push>
mock_requests_get = <function request at 0x7f13f4a5d080>
mock_get_access_credentials = <function get_access_credentials at 0x7f13f4a5d6c0>

    @mock.patch('azure.cli.command_modules.acr.helm.get_access_credentials', autospec=True)
    @mock.patch('requests.request', autospec=True)
    def test_helm_push(self, mock_requests_get, mock_get_access_credentials):
        cmd = self.setup_cmd()
    
        response = mock.MagicMock()
        response.headers = {}
        response.status_code = 200
        mock_requests_get.return_value = response
    
        mock_get_access_credentials.return_value = 'testregistry.azurecr.io', EMPTY_GUID, 'password'
    
        builtins_open = 'builtin.open' if sys.version_info[0] < 3 else 'builtins.open'
    
        # Push a chart
        with mock.patch(builtins_open) as mock_open:
            mock_open.return_value = mock.MagicMock()
>           acr_helm_push(cmd, 'testregistry', './charts/mychart1-0.2.1.tgz', repository='testrepository')

src/azure-cli/azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:647: 
                                        
src/azure-cli/azure/cli/command_modules/acr/helm.py:137: in acr_helm_push
    result = request_data_from_registry(
                                       _ 

http_method = 'put', login_server = 'testregistry.azurecr.io'
path = '/helm/v1/testrepository/_blobs/mychart1-0.2.1.tgz'
username = '00000000-0000-0000-0000-000000000000', password = 'password'
result_index = None, json_payload = None
file_payload = './charts/mychart1-0.2.1.tgz', params = None
manifest_headers = False, raw = False, retry_times = 3, retry_interval = 5
timeout = 300

    def request_data_from_registry(http_method,
                                   login_server,
                                   path,
                                   username,
                                   password,
                                   result_index=None,
                                   json_payload=None,
                                   file_payload=None,
                                   params=None,
                                   manifest_headers=False,
                                   raw=False,
                                   retry_times=3,
                                   retry_interval=5,
                                   timeout=300):
        if http_method not in ALLOWED_HTTP_METHOD:
            raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD))
    
        if json_payload and file_payload:
            raise ValueError("One of json_payload and file_payload can be specified.")
    
        if http_method in ['get', 'delete'] and (json_payload or file_payload):
            raise ValueError("Empty payload is required for http method: {}".format(http_method))
    
        if http_method in ['patch', 'put'] and not (json_payload or file_payload):
            raise ValueError("Non-empty payload is required for http method: {}".format(http_method))
    
        url = 'https://{}{}'.format(login_server, path)
    
        if manifest_headers:
            headers = get_manifest_authorization_header(username, password)
        else:
            headers = get_authorization_header(username, password)
    
        for i in range(0, retry_times):
            errorMessage = None
            try:
                if file_payload:
                    with open(file_payload, 'rb') as data_payload:
                        logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                        response = session.request(
                            method=http_method,
                            url=url,
                            headers=headers,
                            params=params,
                            data=data_payload,
                            timeout=timeout,
                            verify=(not should_disable_connection_verify())
                        )
                else:
                    logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                    response = session.request(
                        method=http_method,
                        url=url,
                        headers=headers,
                        params=params,
                        json=json_payload,
                        timeout=timeout,
                        verify=(not should_disable_connection_verify())
                    )
    
                log_registry_response(response)
    
                if manifest_headers and raw and response.status_code == 200:
                    return response.content.decode('utf-8'), None, response.status_code
                if response.status_code == 200:
                    result = response.json()[result_index] if result_index else response.json()
                    next_link = response.headers['link'] if 'link' in response.headers else None
                    return result, next_link, response.status_code
                if response.status_code == 201 or response.status_code == 202:
                    result = None
                    try:
                        result = response.json()[result_index] if result_index else response.json()
                    except ValueError as e:
                        logger.debug('Response is empty or is not a valid json. Exception: %s', str(e))
                    return result, None, response.status_code
                if response.status_code == 204:
                    return None, None, response.status_code
                if response.status_code == 401:
>                   raise RegistryException(
                        parse_error_message('Authentication required.', response),
                        response.status_code)
E                       azure.cli.command_modules.acr._docker_utils.RegistryException: 2025-01-26 02:33:49.113961 Error: authentication required, visit https://aka.ms/acr/authorization&nbsp;for&nbsp;more&nbsp;information.&nbsp;Correlation&nbsp;ID:&nbsp;da31e10d-a11a-4b51-bda1-003aac205644.

src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py:637: RegistryException
azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:629
Failed test_helm_show The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:550
Failed test_repository_delete The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:317
Failed test_repository_list The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:50
Failed test_repository_show The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:253
Failed test_repository_show_manifests The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:147
Failed test_repository_show_tags The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:91
❌3.9
Type Test Case Error Message Line
Failed test_helm_delete self = <azure.cli.command_modules.acr.tests.hybrid_2020_09_01.test_acr_commands_mock.AcrMockCommandsTests testMethod=test_helm_delete>
mock_requests_get = <function request at 0x7facc75b4670>
mock_get_access_credentials = <function get_access_credentials at 0x7facc75b4b80>

    @mock.patch('azure.cli.command_modules.acr.helm.get_access_credentials', autospec=True)
    @mock.patch('requests.request', autospec=True)
    def test_helm_delete(self, mock_requests_get, mock_get_access_credentials):
        cmd = self.setup_cmd()
    
        response = mock.MagicMock()
        response.headers = {}
        response.status_code = 200
        mock_requests_get.return_value = response
    
        mock_get_access_credentials.return_value = 'testregistry.azurecr.io', EMPTY_GUID, 'password'
    
        # Delete all versions of a chart
>       acr_helm_delete(cmd, 'testregistry', 'mychart1', repository='testrepository', yes=True)

src/azure-cli/azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:609: 
                                        
src/azure-cli/azure/cli/command_modules/acr/helm.py:103: in acr_helm_delete
    return request_data_from_registry(
                                       _ 

http_method = 'delete', login_server = 'testregistry.azurecr.io'
path = '/helm/v1/testrepository/_charts/mychart1'
username = '00000000-0000-0000-0000-000000000000', password = 'password'
result_index = None, json_payload = None, file_payload = None, params = None
manifest_headers = False, raw = False, retry_times = 3, retry_interval = 5
timeout = 300

    def request_data_from_registry(http_method,
                                   login_server,
                                   path,
                                   username,
                                   password,
                                   result_index=None,
                                   json_payload=None,
                                   file_payload=None,
                                   params=None,
                                   manifest_headers=False,
                                   raw=False,
                                   retry_times=3,
                                   retry_interval=5,
                                   timeout=300):
        if http_method not in ALLOWED_HTTP_METHOD:
            raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD))
    
        if json_payload and file_payload:
            raise ValueError("One of json_payload and file_payload can be specified.")
    
        if http_method in ['get', 'delete'] and (json_payload or file_payload):
            raise ValueError("Empty payload is required for http method: {}".format(http_method))
    
        if http_method in ['patch', 'put'] and not (json_payload or file_payload):
            raise ValueError("Non-empty payload is required for http method: {}".format(http_method))
    
        url = 'https://{}{}'.format(login_server, path)
    
        if manifest_headers:
            headers = get_manifest_authorization_header(username, password)
        else:
            headers = get_authorization_header(username, password)
    
        for i in range(0, retry_times):
            errorMessage = None
            try:
                if file_payload:
                    with open(file_payload, 'rb') as data_payload:
                        logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                        response = session.request(
                            method=http_method,
                            url=url,
                            headers=headers,
                            params=params,
                            data=data_payload,
                            timeout=timeout,
                            verify=(not should_disable_connection_verify())
                        )
                else:
                    logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                    response = session.request(
                        method=http_method,
                        url=url,
                        headers=headers,
                        params=params,
                        json=json_payload,
                        timeout=timeout,
                        verify=(not should_disable_connection_verify())
                    )
    
                log_registry_response(response)
    
                if manifest_headers and raw and response.status_code == 200:
                    return response.content.decode('utf-8'), None, response.status_code
                if response.status_code == 200:
                    result = response.json()[result_index] if result_index else response.json()
                    next_link = response.headers['link'] if 'link' in response.headers else None
                    return result, next_link, response.status_code
                if response.status_code == 201 or response.status_code == 202:
                    result = None
                    try:
                        result = response.json()[result_index] if result_index else response.json()
                    except ValueError as e:
                        logger.debug('Response is empty or is not a valid json. Exception: %s', str(e))
                    return result, None, response.status_code
                if response.status_code == 204:
                    return None, None, response.status_code
                if response.status_code == 401:
>                   raise RegistryException(
                        parse_error_message('Authentication required.', response),
                        response.status_code)
E                       azure.cli.command_modules.acr._docker_utils.RegistryException: 2025-01-26 02:32:01.979752 Error: authentication required, visit https://aka.ms/acr/authorization&nbsp;for&nbsp;more&nbsp;information.&nbsp;Correlation&nbsp;ID:&nbsp;714a3d91-ebe7-4fe1-932f-bddd81f3bf7d.

src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py:637: RegistryException
azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:595
Failed test_helm_list self = <azure.cli.command_modules.acr.tests.hybrid_2020_09_01.test_acr_commands_mock.AcrMockCommandsTests testMethod=test_helm_list>
mock_requests_get = <function request at 0x7facc75b4670>
mock_get_access_credentials = <function get_access_credentials at 0x7facc75b4b80>

    @mock.patch('azure.cli.command_modules.acr.helm.get_access_credentials', autospec=True)
    @mock.patch('requests.request', autospec=True)
    def test_helm_list(self, mock_requests_get, mock_get_access_credentials):
        cmd = self.setup_cmd()
    
        response = mock.MagicMock()
        response.headers = {}
        response.status_code = 200
        response.content = json.dumps({
            'mychart1': [
                {
                    'name': 'mychart1',
                    'version': '0.2.1'
                },
                {
                    'name': 'mychart1',
                    'version': '0.1.2'
                }
            ],
            'mychart2': [
                {
                    'name': 'mychart2',
                    'version': '2.1.0'
                }
            ]}).encode()
        mock_requests_get.return_value = response
    
        mock_get_access_credentials.return_value = 'testregistry.azurecr.io', EMPTY_GUID, 'password'
>       acr_helm_list(cmd, 'testregistry', repository='testrepository')

src/azure-cli/azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:541: 
                                        
src/azure-cli/azure/cli/command_modules/acr/helm.py:42: in acr_helm_list
    return request_data_from_registry(
                                       _ 

http_method = 'get', login_server = 'testregistry.azurecr.io'
path = '/helm/v1/testrepository/_charts'
username = '00000000-0000-0000-0000-000000000000', password = 'password'
result_index = None, json_payload = None, file_payload = None, params = None
manifest_headers = False, raw = False, retry_times = 3, retry_interval = 5
timeout = 300

    def request_data_from_registry(http_method,
                                   login_server,
                                   path,
                                   username,
                                   password,
                                   result_index=None,
                                   json_payload=None,
                                   file_payload=None,
                                   params=None,
                                   manifest_headers=False,
                                   raw=False,
                                   retry_times=3,
                                   retry_interval=5,
                                   timeout=300):
        if http_method not in ALLOWED_HTTP_METHOD:
            raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD))
    
        if json_payload and file_payload:
            raise ValueError("One of json_payload and file_payload can be specified.")
    
        if http_method in ['get', 'delete'] and (json_payload or file_payload):
            raise ValueError("Empty payload is required for http method: {}".format(http_method))
    
        if http_method in ['patch', 'put'] and not (json_payload or file_payload):
            raise ValueError("Non-empty payload is required for http method: {}".format(http_method))
    
        url = 'https://{}{}'.format(login_server, path)
    
        if manifest_headers:
            headers = get_manifest_authorization_header(username, password)
        else:
            headers = get_authorization_header(username, password)
    
        for i in range(0, retry_times):
            errorMessage = None
            try:
                if file_payload:
                    with open(file_payload, 'rb') as data_payload:
                        logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                        response = session.request(
                            method=http_method,
                            url=url,
                            headers=headers,
                            params=params,
                            data=data_payload,
                            timeout=timeout,
                            verify=(not should_disable_connection_verify())
                        )
                else:
                    logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                    response = session.request(
                        method=http_method,
                        url=url,
                        headers=headers,
                        params=params,
                        json=json_payload,
                        timeout=timeout,
                        verify=(not should_disable_connection_verify())
                    )
    
                log_registry_response(response)
    
                if manifest_headers and raw and response.status_code == 200:
                    return response.content.decode('utf-8'), None, response.status_code
                if response.status_code == 200:
                    result = response.json()[result_index] if result_index else response.json()
                    next_link = response.headers['link'] if 'link' in response.headers else None
                    return result, next_link, response.status_code
                if response.status_code == 201 or response.status_code == 202:
                    result = None
                    try:
                        result = response.json()[result_index] if result_index else response.json()
                    except ValueError as e:
                        logger.debug('Response is empty or is not a valid json. Exception: %s', str(e))
                    return result, None, response.status_code
                if response.status_code == 204:
                    return None, None, response.status_code
                if response.status_code == 401:
>                   raise RegistryException(
                        parse_error_message('Authentication required.', response),
                        response.status_code)
E                       azure.cli.command_modules.acr._docker_utils.RegistryException: 2025-01-26 02:32:02.482096 Error: authentication required, visit https://aka.ms/acr/authorization&nbsp;for&nbsp;more&nbsp;information.&nbsp;Correlation&nbsp;ID:&nbsp;3a6bcae6-45f3-4784-91b7-702e72b167d6.

src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py:637: RegistryException
azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:512
Failed test_helm_push self = <azure.cli.command_modules.acr.tests.hybrid_2020_09_01.test_acr_commands_mock.AcrMockCommandsTests testMethod=test_helm_push>
mock_requests_get = <function request at 0x7facc75b4670>
mock_get_access_credentials = <function get_access_credentials at 0x7facc75b4b80>

    @mock.patch('azure.cli.command_modules.acr.helm.get_access_credentials', autospec=True)
    @mock.patch('requests.request', autospec=True)
    def test_helm_push(self, mock_requests_get, mock_get_access_credentials):
        cmd = self.setup_cmd()
    
        response = mock.MagicMock()
        response.headers = {}
        response.status_code = 200
        mock_requests_get.return_value = response
    
        mock_get_access_credentials.return_value = 'testregistry.azurecr.io', EMPTY_GUID, 'password'
    
        builtins_open = 'builtin.open' if sys.version_info[0] < 3 else 'builtins.open'
    
        # Push a chart
        with mock.patch(builtins_open) as mock_open:
            mock_open.return_value = mock.MagicMock()
>           acr_helm_push(cmd, 'testregistry', './charts/mychart1-0.2.1.tgz', repository='testrepository')

src/azure-cli/azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:647: 
                                        
src/azure-cli/azure/cli/command_modules/acr/helm.py:137: in acr_helm_push
    result = request_data_from_registry(
                                       _ 

http_method = 'put', login_server = 'testregistry.azurecr.io'
path = '/helm/v1/testrepository/_blobs/mychart1-0.2.1.tgz'
username = '00000000-0000-0000-0000-000000000000', password = 'password'
result_index = None, json_payload = None
file_payload = './charts/mychart1-0.2.1.tgz', params = None
manifest_headers = False, raw = False, retry_times = 3, retry_interval = 5
timeout = 300

    def request_data_from_registry(http_method,
                                   login_server,
                                   path,
                                   username,
                                   password,
                                   result_index=None,
                                   json_payload=None,
                                   file_payload=None,
                                   params=None,
                                   manifest_headers=False,
                                   raw=False,
                                   retry_times=3,
                                   retry_interval=5,
                                   timeout=300):
        if http_method not in ALLOWED_HTTP_METHOD:
            raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD))
    
        if json_payload and file_payload:
            raise ValueError("One of json_payload and file_payload can be specified.")
    
        if http_method in ['get', 'delete'] and (json_payload or file_payload):
            raise ValueError("Empty payload is required for http method: {}".format(http_method))
    
        if http_method in ['patch', 'put'] and not (json_payload or file_payload):
            raise ValueError("Non-empty payload is required for http method: {}".format(http_method))
    
        url = 'https://{}{}'.format(login_server, path)
    
        if manifest_headers:
            headers = get_manifest_authorization_header(username, password)
        else:
            headers = get_authorization_header(username, password)
    
        for i in range(0, retry_times):
            errorMessage = None
            try:
                if file_payload:
                    with open(file_payload, 'rb') as data_payload:
                        logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                        response = session.request(
                            method=http_method,
                            url=url,
                            headers=headers,
                            params=params,
                            data=data_payload,
                            timeout=timeout,
                            verify=(not should_disable_connection_verify())
                        )
                else:
                    logger.debug(add_timestamp("Sending a HTTP {} request to {}".format(http_method, url)))
                    response = session.request(
                        method=http_method,
                        url=url,
                        headers=headers,
                        params=params,
                        json=json_payload,
                        timeout=timeout,
                        verify=(not should_disable_connection_verify())
                    )
    
                log_registry_response(response)
    
                if manifest_headers and raw and response.status_code == 200:
                    return response.content.decode('utf-8'), None, response.status_code
                if response.status_code == 200:
                    result = response.json()[result_index] if result_index else response.json()
                    next_link = response.headers['link'] if 'link' in response.headers else None
                    return result, next_link, response.status_code
                if response.status_code == 201 or response.status_code == 202:
                    result = None
                    try:
                        result = response.json()[result_index] if result_index else response.json()
                    except ValueError as e:
                        logger.debug('Response is empty or is not a valid json. Exception: %s', str(e))
                    return result, None, response.status_code
                if response.status_code == 204:
                    return None, None, response.status_code
                if response.status_code == 401:
>                   raise RegistryException(
                        parse_error_message('Authentication required.', response),
                        response.status_code)
E                       azure.cli.command_modules.acr._docker_utils.RegistryException: 2025-01-26 02:32:02.914315 Error: authentication required, visit https://aka.ms/acr/authorization&nbsp;for&nbsp;more&nbsp;information.&nbsp;Correlation&nbsp;ID:&nbsp;e805162c-d67c-4f24-ba5c-bb47f879f9f9.

src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py:637: RegistryException
azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:629
Failed test_helm_show The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:550
Failed test_repository_delete The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:317
Failed test_repository_list The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:50
Failed test_repository_show The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:253
Failed test_repository_show_manifests The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:147
Failed test_repository_show_tags The error message is too long, please check the pipeline log for details. azure/cli/command_modules/acr/tests/hybrid_2020_09_01/test_acr_commands_mock.py:91
️✔️latest
️✔️3.12
️✔️3.9
️✔️acs
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.9
️✔️ams
️✔️latest
️✔️3.12
️✔️3.9
️✔️apim
️✔️latest
️✔️3.12
️✔️3.9
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.9
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.9
️✔️aro
️✔️latest
️✔️3.12
️✔️3.9
️✔️backup
️✔️latest
️✔️3.12
️✔️3.9
️✔️batch
️✔️latest
️✔️3.12
️✔️3.9
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.9
️✔️billing
️✔️latest
️✔️3.12
️✔️3.9
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.9
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.9
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.9
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.9
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.9
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.9
️✔️config
️✔️latest
️✔️3.12
️✔️3.9
️✔️configure
️✔️latest
️✔️3.12
️✔️3.9
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.9
️✔️container
️✔️latest
️✔️3.12
️✔️3.9
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.9
️✔️core
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.9
️✔️databoxedge
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️dls
️✔️latest
️✔️3.12
️✔️3.9
️✔️dms
️✔️latest
️✔️3.12
️✔️3.9
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.9
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.9
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.9
️✔️find
️✔️latest
️✔️3.12
️✔️3.9
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.9
️✔️identity
️✔️latest
️✔️3.12
️✔️3.9
️✔️iot
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️keyvault
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️lab
️✔️latest
️✔️3.12
️✔️3.9
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.9
️✔️maps
️✔️latest
️✔️3.12
️✔️3.9
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.9
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.9
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.9
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.9
️✔️network
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.9
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.9
️✔️profile
️✔️latest
️✔️3.12
️✔️3.9
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.9
️✔️redis
️✔️latest
️✔️3.12
️✔️3.9
️✔️relay
️✔️latest
️✔️3.12
️✔️3.9
️✔️resource
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️role
️✔️latest
️✔️3.12
️✔️3.9
️✔️search
️✔️latest
️✔️3.12
️✔️3.9
️✔️security
️✔️latest
️✔️3.12
️✔️3.9
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.9
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.9
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.9
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.9
️✔️sql
️✔️latest
️✔️3.12
️✔️3.9
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.9
️✔️storage
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.9
️✔️telemetry
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️util
️✔️latest
️✔️3.12
️✔️3.9
️✔️vm
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9

Copy link

azure-client-tools-bot-prd bot commented Dec 15, 2024

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Collaborator

yonzhan commented Dec 15, 2024

Thank you for your contribution! We will review the pull request and get back to you soon.

Copy link

⚠️Your changes in this PR will be released on Jan 14, 2025 due to CCOA (extend to Jan 6, 2025)

@yanzhudd
Copy link
Contributor

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@yanzhudd
Copy link
Contributor

Please rerun some related tests in live mode to verify this change.

@yanzhudd
Copy link
Contributor

Please note Azure CLI will freeze the code on 01/28/2025 10:00 UTC for the upcoming release. If you want to catch this release train, please get this PR ready ASAP, otherwise it has to be postponed to next sprint.

@oxpa
Copy link
Author

oxpa commented Jan 24, 2025

@microsoft-github-policy-service agree

@oxpa
Copy link
Author

oxpa commented Jan 24, 2025

@yanzhudd i didn't realize there are tests available to me. I made sure to run them locally and it should all be good now.

@yanzhudd
Copy link
Contributor

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@yanzhudd
Copy link
Contributor

please fix the CI issues

@oxpa
Copy link
Author

oxpa commented Jan 26, 2025

@yanzhudd pipelines run tests without my patches and I can't fix them otherwise, I think.

Tests mock requests.request object. While the code now uses azure.cli.command_modules.acr._docker_utils.session.request object. I changed tests to match the code in my last commit but it was not applied to tests in pipelines.

@toddysm
Copy link

toddysm commented Jan 28, 2025

@johnsonshi This is performance related. Can you please take a look at it?

@yanzhudd
Copy link
Contributor

/azp run

@johnsonshi
Copy link

@m5i-work, can you take a look at this PR as it relates to a perf improvement using connection pooling when connecting to ACR using cli?

@@ -34,6 +35,7 @@


logger = get_logger(__name__)
session = Session()
Copy link
Member

Choose a reason for hiding this comment

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

Instead of a global var, can we move this to parameters of request_data_from_registry()? The new parameter can default to None. We can make this PR scoped to repository listing and revisit other call sites of request_data_from_registry() that can benefit from connection pooling later.

@m5i-work
Copy link
Member

@oxpa Thanks for looking into this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auto-Assign Auto assign by bot Container Registry az acr customer-reported Issues that are reported by GitHub users external to the Azure organization.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants