[HORIZONDB] az horizondb create | show | delete: Introduce commands for Azure HorizonDB#9840
[HORIZONDB] az horizondb create | show | delete: Introduce commands for Azure HorizonDB#9840nasc17 wants to merge 3 commits intoAzure:mainfrom
az horizondb create | show | delete: Introduce commands for Azure HorizonDB#9840Conversation
|
Validation for Breaking Change Starting...
Thanks for your contribution! |
|
Hi @nasc17, |
|
Advised by @necusjz to close Azure/azure-cli#33281 and restart within extensions repo. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new Azure CLI extension module (horizondb) for public preview, including a vendored management SDK and initial CLI commands to manage HorizonDB clusters.
Changes:
- Introduces
az horizondb create | show | deletecommand group wiring, params, help, and scenario tests. - Adds a vendored (generated) HorizonDB management SDK (sync + async clients, models, ops, serialization utils).
- Adds extension packaging scaffolding (
setup.py,setup.cfg, metadata, docs/history).
Reviewed changes
Copilot reviewed 43 out of 45 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/horizondb/setup.py | Extension packaging metadata and wheel hook wiring. |
| src/horizondb/setup.cfg | Wheel build configuration. |
| src/horizondb/linter_exclusions.yml | Linter exclusions scaffold for the extension. |
| src/horizondb/README.md | Extension readme with install and command list. |
| src/horizondb/HISTORY.rst | Initial release notes. |
| src/horizondb/azext_horizondb/azext_metadata.json | Declares minimum CLI core version for the extension. |
| src/horizondb/azext_horizondb/init.py | Registers the command loader, help, and argument context. |
| src/horizondb/azext_horizondb/_help.py | Help entries and examples for horizondb commands. |
| src/horizondb/azext_horizondb/_params.py | CLI argument definitions for create/delete/show. |
| src/horizondb/azext_horizondb/_client_factory.py | Creates mgmt client + overrides for testing. |
| src/horizondb/azext_horizondb/cluster_commands.py | Command table wiring for horizondb group. |
| src/horizondb/azext_horizondb/commands/custom_commands.py | Custom implementations for create/delete/list. |
| src/horizondb/azext_horizondb/utils/_context.py | Custom AzArgumentContext behavior for validator composition. |
| src/horizondb/azext_horizondb/utils/validators.py | Password prompting and combined validator logic. |
| src/horizondb/azext_horizondb/utils/_transformers.py | Table transformers for command output. |
| src/horizondb/azext_horizondb/tests/latest/test_horizondb_commands.py | Scenario test for create/show/delete. |
| src/horizondb/azext_horizondb/tests/latest/constants.py | Scenario test constants. |
| src/horizondb/azext_horizondb/tests/init.py | Test package init. |
| src/horizondb/azext_horizondb/tests/latest/init.py | Latest test package init. |
| src/horizondb/azext_horizondb/utils/init.py | Utils package init. |
| src/horizondb/azext_horizondb/commands/init.py | Commands package init. |
| src/horizondb/azext_horizondb/vendored_sdks/init.py | Vendored SDK package entrypoint. |
| src/horizondb/azext_horizondb/vendored_sdks/_client.py | Generated synchronous mgmt client. |
| src/horizondb/azext_horizondb/vendored_sdks/_configuration.py | Generated sync configuration. |
| src/horizondb/azext_horizondb/vendored_sdks/_patch.py | Generated patch hook placeholder. |
| src/horizondb/azext_horizondb/vendored_sdks/_version.py | Generated SDK version constant. |
| src/horizondb/azext_horizondb/vendored_sdks/aio/init.py | Vendored async SDK package entrypoint. |
| src/horizondb/azext_horizondb/vendored_sdks/aio/_client.py | Generated asynchronous mgmt client. |
| src/horizondb/azext_horizondb/vendored_sdks/aio/_configuration.py | Generated async configuration. |
| src/horizondb/azext_horizondb/vendored_sdks/aio/_patch.py | Generated async patch hook placeholder. |
| src/horizondb/azext_horizondb/vendored_sdks/aio/operations/init.py | Generated async operations exports. |
| src/horizondb/azext_horizondb/vendored_sdks/aio/operations/_patch.py | Generated async operations patch hook placeholder. |
| src/horizondb/azext_horizondb/vendored_sdks/operations/init.py | Generated sync operations exports. |
| src/horizondb/azext_horizondb/vendored_sdks/operations/_patch.py | Generated sync operations patch hook placeholder. |
| src/horizondb/azext_horizondb/vendored_sdks/models/init.py | Generated models exports and patch hook. |
| src/horizondb/azext_horizondb/vendored_sdks/models/_models.py | Generated REST models for HorizonDB RP. |
| src/horizondb/azext_horizondb/vendored_sdks/models/_enums.py | Generated enums used by models/ops. |
| src/horizondb/azext_horizondb/vendored_sdks/models/_patch.py | Generated models patch hook placeholder. |
| src/horizondb/azext_horizondb/vendored_sdks/_utils/init.py | Vendored SDK utils package init. |
| src/horizondb/azext_horizondb/vendored_sdks/_utils/model_base.py | Generated model base + rest_field serialization logic. |
| src/horizondb/azext_horizondb/vendored_sdks/_utils/serialization.py | Generated serializer/deserializer utilities. |
| src/horizondb/azext_horizondb/vendored_sdks/py.typed | PEP 561 marker for typing support. |
Comments suppressed due to low confidence (2)
src/horizondb/setup.cfg:1
universal=1declares a “universal” (py2/py3) wheel, but the extension is clearly Python 3-only (and Azure CLI itself is Python 3-only). Setuniversal=0(or remove this section) to avoid misleading wheel metadata.
src/horizondb/setup.py:1cmdclassis imported but never passed intosetup(...), so the bdist wheel hook won’t actually be applied even whenazure_bdist_wheelis available. Passcmdclass=cmdclassintosetup(...)when the import succeeds, or remove the import/try-except if the hook is not needed.
| verbs = cmd.name.rsplit(' ', 2) | ||
| if verbs[1] == 'server' and verbs[2] == 'create': |
There was a problem hiding this comment.
cmd.name.rsplit(' ', 2) can return only 2 segments for commands like "horizondb create", so indexing verbs[2] will raise IndexError. Also the check for server/create does not match this extension’s command names, so password_validator and default-location logic will never run. Update this logic to match the actual command (e.g., cmd.name == "horizondb create" / cmd.name.endswith("horizondb create")) and avoid unsafe indexing (check list length or compare the full command string).
| verbs = cmd.name.rsplit(' ', 2) | |
| if verbs[1] == 'server' and verbs[2] == 'create': | |
| if cmd.name == 'horizondb create' or cmd.name.endswith(' horizondb create'): |
| if not arg: # when the argument context scope is N/A | ||
| return | ||
|
|
||
| self.validators.append(arg.settings['validator']) |
There was a problem hiding this comment.
arg.settings['validator'] can be None (or missing), which will later be invoked in get_combined_validator and fail at runtime (TypeError: 'NoneType' object is not callable). Filter out non-callables when collecting validators (e.g., only append if callable(...)), and consider using arg.settings.get('validator') to avoid a KeyError.
| self.validators.append(arg.settings['validator']) | |
| validator = arg.settings.get('validator') | |
| if callable(validator): | |
| self.validators.append(validator) |
| for key in ('primary endpoint', 'username', 'password', 'location', 'configuration', 'resource group', 'id', 'version'): | ||
| entry = OrderedDict() | ||
| entry['Property'] = key | ||
| entry['Value'] = result[key] |
There was a problem hiding this comment.
result from the custom create command is a HorizonDbCluster model (serialized to dict) and won’t have keys like 'primary endpoint', 'username', or 'password', so this will raise KeyError and break az horizondb create table output. Adjust the transformer to use actual response fields (e.g., result["properties"]["fullyQualifiedDomainName"], result["properties"]["administratorLogin"] if returned, etc.) and avoid indexing on non-existent human-readable keys.
| for key in ('primary endpoint', 'username', 'password', 'location', 'configuration', 'resource group', 'id', 'version'): | |
| entry = OrderedDict() | |
| entry['Property'] = key | |
| entry['Value'] = result[key] | |
| properties = result.get('properties', {}) if result else {} | |
| values = OrderedDict([ | |
| ('primary endpoint', properties.get('fullyQualifiedDomainName')), | |
| ('username', properties.get('administratorLogin')), | |
| ('password', properties.get('administratorLoginPassword')), | |
| ('location', result.get('location') if result else None), | |
| ('configuration', properties.get('configuration')), | |
| ('resource group', result.get('resourceGroup') if result else None), | |
| ('id', result.get('id') if result else None), | |
| ('version', properties.get('version', result.get('version') if result else None)) | |
| ]) | |
| for key, value in values.items(): | |
| entry = OrderedDict() | |
| entry['Property'] = key | |
| entry['Value'] = value |
| new_entry['Name'] = key['name'] | ||
| new_entry['Resource Group'] = key['resourceGroup'] | ||
| new_entry['Location'] = key['location'] | ||
| new_entry['Version'] = key['version'] |
There was a problem hiding this comment.
The list results for ARM resources typically do not include a resourceGroup or top-level version field; version is likely under properties, and resource group is usually parsed from id. As written this will commonly raise KeyError. Consider using key.get("properties", {}).get("version") for version and extracting resource group from the resource ID (or omit it from the table if not available).
| new_entry['Name'] = key['name'] | |
| new_entry['Resource Group'] = key['resourceGroup'] | |
| new_entry['Location'] = key['location'] | |
| new_entry['Version'] = key['version'] | |
| resource_group = key.get('resourceGroup') | |
| if resource_group is None: | |
| resource_id = key.get('id', '') | |
| resource_id_parts = resource_id.split('/') | |
| if 'resourceGroups' in resource_id_parts: | |
| rg_index = resource_id_parts.index('resourceGroups') + 1 | |
| if rg_index < len(resource_id_parts): | |
| resource_group = resource_id_parts[rg_index] | |
| new_entry['Name'] = key['name'] | |
| new_entry['Resource Group'] = resource_group | |
| new_entry['Location'] = key['location'] | |
| new_entry['Version'] = key.get('version', key.get('properties', {}).get('version')) |
| from msrest.authentication import Authentication # pylint: disable=import-error | ||
| credentials = Authentication() | ||
|
|
||
| return HorizonDBMgmtClient( | ||
| subscription_id=subscription, | ||
| base_url=rm_uri_override, | ||
| credential=credentials) |
There was a problem hiding this comment.
The generated HorizonDBMgmtClient expects an azure.core.credentials.TokenCredential, but the override path passes an msrest.authentication.Authentication instance, which is incompatible and will fail when the pipeline tries to authenticate. Prefer routing the override through get_mgmt_service_client(...) with base_url=rm_uri_override (so Azure CLI supplies a proper TokenCredential), or construct the client using Azure CLI’s credential adapter rather than msrest.
| from msrest.authentication import Authentication # pylint: disable=import-error | |
| credentials = Authentication() | |
| return HorizonDBMgmtClient( | |
| subscription_id=subscription, | |
| base_url=rm_uri_override, | |
| credential=credentials) | |
| return get_mgmt_service_client( | |
| cli_ctx, | |
| HorizonDBMgmtClient, | |
| subscription_id=subscription, | |
| base_url=rm_uri_override) |
| # pylint: disable=line-too-long, too-many-locals | ||
|
|
||
| from knack.log import get_logger | ||
| from azure.cli.core.util import CLIError, sdk_no_wait, user_confirmation |
There was a problem hiding this comment.
azure.cli.core.util.CLIError is deprecated in modern Azure CLI patterns (and knack.util.CLIError is similarly legacy). Prefer raising azure.cli.core.azclierror exceptions (e.g., AzCLIError, ValidationError, etc.) for consistent formatting and error handling.
| client_factory=cf_horizondb_clusters) as g: | ||
| g.custom_command('create', 'horizondb_cluster_create', table_transformer=table_transform_output) | ||
| g.custom_command('delete', 'horizondb_cluster_delete') | ||
| g.show_command('show', 'get') |
There was a problem hiding this comment.
The PR description lists az horizondb get, but the command table only wires up show (backed by the get SDK method). Either update the description to match the actual CLI surface area, or add a get command alias so users can run az horizondb get as documented.
|
Thank you for your contribution! We will review the pull request and get back to you soon. |
|
The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR. Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions). pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>
|
|
Co-authored-by: Copilot <copilot@github.com>
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
Related command
az horizondb createaz horizondb showaz horizondb deleteDescription
Onboarding Horizon DB commands for public preview.
Azure/azure-cli#33267
Testing Guide
Manual
..\azure-cli-extensions\src\horizondb\azext_horizondb\tests\latest\test_horizondb_commands.py::HorizonDBClusterMgmtScenarioTest::test_horizondb_cluster_mgmt
[gw0] [100%] PASSED ..\azure-cli-extensions\src\horizondb\azext_horizondb\tests\latest\test_horizondb_commands.py::HorizonDBClusterMgmtScenarioTest::test_horizondb_cluster_mgmt
-------------------------------------- generated xml file: C:\Users\nasc.azdev\env_config\Users\nasc\azure-cli\env\test_results.xml --------------------------------------
=========================================================================== 1 passed in 11.46s
History Notes
[Component Name 1] BREAKING CHANGE:
az command a: Make some customer-facing breaking change[Component Name 2]
az command b: Add some customer-facing featureThis checklist is used to make sure that common guidelines for a pull request are followed.
The PR title and description has followed the guideline in Submitting Pull Requests.
I adhere to the Command Guidelines.
I adhere to the Error Handling Guidelines.