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
11 changes: 11 additions & 0 deletions src/aimanager/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
Release History
===============

1.2.1b1
+++++++
* Add ``az aimanager model`` commands (``show``, ``list`` and ``calculate-cost``) to browse the
regional AI model catalog and estimate the cost of deploying a model.
* Add ``az aimanager modelsource`` commands (``add``, ``update``, ``list``, ``show``,
``delete`` and ``wait``) to manage the model sources of an AI Manager.
* ``az aimanager namespace``: Add ``list-accesskeys`` and ``rotate-accesskeys`` commands to
read and rotate the namespace LLM gateway API keys.
* ``az aimanager namespace``: Accept ``--aimanager-name`` as an alias of ``--manager``/``-m``
for consistency with ``az aimanager namespace modeldeployment``.

1.2.0
++++++
* Add ``az aimanager namespace modeldeployment`` commands to add, update, list, show, delete,
Expand Down
27 changes: 27 additions & 0 deletions src/aimanager/azext_aimanager/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,33 @@
text: az aimanager namespace get-credentials -m my-ai-manager -g myrg --name team-alpha -f -
"""

helps['aimanager namespace list-accesskeys'] = """
type: command
short-summary: List the LLM gateway endpoint and API keys of an AI Manager namespace.
long-summary: |-
Returns the namespace-scoped, OpenAI-compatible inference gateway endpoint together with the
current primary and secondary API keys. Treat the keys as secrets: do not log them or persist
them in plaintext.
examples:
- name: List the access keys of a namespace
text: az aimanager namespace list-accesskeys -g myrg --aimanager-name my-ai-manager --name team-alpha
- name: Show only the gateway endpoint
text: az aimanager namespace list-accesskeys -g myrg --aimanager-name my-ai-manager --name team-alpha --query endpoint -o tsv
"""

helps['aimanager namespace rotate-accesskeys'] = """
type: command
short-summary: Rotate the LLM gateway API keys of an AI Manager namespace.
long-summary: |-
A new key is generated and installed as the primary key, and the previous primary key
overwrites the secondary key so clients can roll over without downtime. Returns the updated
access info. Any client still using the previous secondary key will stop being able to
authenticate.
examples:
- name: Rotate the access keys of a namespace
text: az aimanager namespace rotate-accesskeys -g myrg --aimanager-name my-ai-manager --name team-alpha
"""

helps['aimanager namespace modeldeployment'] = """
type: group
short-summary: Manage model deployments within an AI Manager namespace.
Expand Down
7 changes: 6 additions & 1 deletion src/aimanager/azext_aimanager/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def load_arguments(self, _):
help='Comma-separated key=value pairs to specify custom headers.')

with self.argument_context('aimanager namespace') as c:
c.argument('ai_manager_name', options_list=['--manager', '-m'],
c.argument('ai_manager_name', options_list=['--aimanager-name', '--manager', '-m'],
validator=validate_ai_manager_name,
help='The name of the AI Manager resource.')
c.argument('namespace_name', options_list=['--name', '-n'],
Expand All @@ -80,6 +80,11 @@ def load_arguments(self, _):
c.argument('aks_custom_headers', options_list=['--aks-custom-headers'],
help='Comma-separated key=value pairs to specify custom headers.')

for scope in ['aimanager namespace list-accesskeys', 'aimanager namespace rotate-accesskeys']:
with self.argument_context(scope) as c:
c.argument('aks_custom_headers', options_list=['--aks-custom-headers'],
help='Comma-separated key=value pairs to specify custom headers.')

with self.argument_context('aimanager namespace modeldeployment') as c:
c.argument('ai_manager_name', options_list=['--aimanager-name'],
validator=validate_ai_manager_name,
Expand Down
2 changes: 1 addition & 1 deletion src/aimanager/azext_aimanager/azext_metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"azext.isPreview": true,
"azext.minCliCoreVersion": "2.61.0",
"version": "1.2.0"
"version": "1.2.1b1"
}
2 changes: 2 additions & 0 deletions src/aimanager/azext_aimanager/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def load_command_table(self, _):
g.custom_command("list", "list_aimanager_namespace")
g.custom_command("delete", "delete_aimanager_namespace", supports_no_wait=True, confirmation=True)
g.custom_command("get-credentials", "aimanager_namespace_get_credentials")
g.custom_command("list-accesskeys", "aimanager_namespace_list_accesskeys")
g.custom_command("rotate-accesskeys", "aimanager_namespace_rotate_accesskeys", confirmation=True)
g.wait_command("wait")

# aimanager namespace modeldeployment command group
Expand Down
24 changes: 24 additions & 0 deletions src/aimanager/azext_aimanager/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,30 @@ def aimanager_namespace_get_credentials(cmd,
resource_group_name, ai_manager_name, namespace_name, headers=headers)
_write_kubeconfig(credential_results, path, overwrite_existing, context_name)


# pylint: disable=unused-argument
def aimanager_namespace_list_accesskeys(cmd,
client,
resource_group_name,
ai_manager_name,
namespace_name,
aks_custom_headers=None):
headers = get_aks_custom_headers(aks_custom_headers)
return client.list_access_keys(
resource_group_name, ai_manager_name, namespace_name, headers=headers)


# pylint: disable=unused-argument
def aimanager_namespace_rotate_accesskeys(cmd,
client,
resource_group_name,
ai_manager_name,
namespace_name,
aks_custom_headers=None):
headers = get_aks_custom_headers(aks_custom_headers)
return client.rotate_keys(
resource_group_name, ai_manager_name, namespace_name, headers=headers)

# endregion


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import unittest
from unittest.mock import MagicMock

from azext_aimanager import custom


class MockCmd:
pass


class TestNamespaceAccessKeys(unittest.TestCase):

def setUp(self):
self.cmd = MockCmd()
self.client = MagicMock()

def test_list_accesskeys(self):
self.client.list_access_keys.return_value = "access-info"

result = custom.aimanager_namespace_list_accesskeys(
self.cmd, self.client, "rg", "manager", "namespace")

self.assertEqual(result, "access-info")
self.client.list_access_keys.assert_called_once_with(
"rg", "manager", "namespace", headers={})

def test_rotate_accesskeys(self):
self.client.rotate_keys.return_value = "rotated"

result = custom.aimanager_namespace_rotate_accesskeys(
self.cmd, self.client, "rg", "manager", "namespace")

self.assertEqual(result, "rotated")
self.client.rotate_keys.assert_called_once_with(
"rg", "manager", "namespace", headers={})

def test_custom_headers_are_forwarded(self):
custom.aimanager_namespace_list_accesskeys(
self.cmd, self.client, "rg", "manager", "namespace",
aks_custom_headers="a=1,b=2")

self.client.list_access_keys.assert_called_once_with(
"rg", "manager", "namespace", headers={"a": "1", "b": "2"})


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from unittest.mock import MagicMock, patch

from azure.cli.testsdk import ScenarioTest

from azext_aimanager.vendored_sdks.v2026_05_02_preview import models


class NamespaceAccessKeysScenarioTest(ScenarioTest):

def test_namespace_accesskeys_commands(self):
access_info = models.NamespaceAccessInfo({
'endpoint': 'https://team-alpha.example.eastus2.aksapp.io/v1',
'primaryKey': 'primary-key-value',
'secondaryKey': 'secondary-key-value',
})
rotated_info = models.NamespaceAccessInfo({
'endpoint': 'https://team-alpha.example.eastus2.aksapp.io/v1',
'primaryKey': 'new-primary-key-value',
'secondaryKey': 'primary-key-value',
'lastRotatedAt': '2026-08-14T00:00:00Z',
})

operations = MagicMock()
operations.list_access_keys.return_value = access_info
operations.rotate_keys.return_value = rotated_info
service_client = MagicMock()
service_client.ai_manager_namespaces = operations

command_prefix = 'aimanager namespace {} -g rg --aimanager-name manager -n namespace'

with patch('azext_aimanager._client_factory.get_aimanager_client',
return_value=service_client):
self.cmd(
command_prefix.format('list-accesskeys'),
checks=[
self.check('endpoint', 'https://team-alpha.example.eastus2.aksapp.io/v1'),
self.check('primaryKey', 'primary-key-value'),
self.check('secondaryKey', 'secondary-key-value'),
])

# the previous primary key is demoted to the secondary key on rotation
self.cmd(
command_prefix.format('rotate-accesskeys') + ' --yes',
checks=[
self.check('primaryKey', 'new-primary-key-value'),
self.check('secondaryKey', 'primary-key-value'),
])

# -m/--manager remains accepted for backwards compatibility
self.cmd(
'aimanager namespace list-accesskeys -g rg -m manager -n namespace',
checks=[self.check('primaryKey', 'primary-key-value')])

operations.list_access_keys.assert_called_with(
'rg', 'manager', 'namespace', headers={})

# --aks-custom-headers is parsed and forwarded to the request
self.cmd(
command_prefix.format('list-accesskeys') + ' --aks-custom-headers a=1,b=2',
checks=[self.check('primaryKey', 'primary-key-value')])

operations.list_access_keys.assert_called_with(
'rg', 'manager', 'namespace', headers={'a': '1', 'b': '2'})
operations.rotate_keys.assert_called_once_with('rg', 'manager', 'namespace', headers={})
2 changes: 1 addition & 1 deletion src/aimanager/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from distutils import log as logger
logger.warn("Wheel is not available, disabling bdist_wheel hook")

VERSION = '1.2.0'
VERSION = '1.2.1b1'
Comment thread
gloriahxr marked this conversation as resolved.

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