From 57b35eb0d91d8b20579cf75c428e0555ae12d503 Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 09:36:48 +0530 Subject: [PATCH 1/7] [workload-orchestration] Auto-install required az CLI extensions The `az workload-orchestration cluster init` command depends on three az CLI extensions at runtime (connectedk8s, k8s-extension, customlocation). If any are missing, sub-command invocations fail with opaque errors. This change adds a Step 0 in `target_prepare()` that ensures all three extensions are installed before preflight, mirroring the `azext_vme.utils.check_and_add_cli_extension` pattern. - common/utils.py: add REQUIRED_CLI_EXTENSIONS, check_and_add_cli_extension, ensure_required_cli_extensions. Uses subprocess (not invoke_cli_command) so freshly installed extensions are picked up correctly. - common/target.py: call ensure_required_cli_extensions() at the start of target_prepare; record step_results["cli-extensions"] = "Ready" on success and surface failures via _print_failure_hint. Tested end-to-end: removed all 3 extensions, ran the auto-install path, verified all 3 reinstalled successfully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../common/target.py | 12 ++++ .../common/utils.py | 71 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/workload-orchestration/azext_workload_orchestration/common/target.py b/src/workload-orchestration/azext_workload_orchestration/common/target.py index 8b6531290a4..7c2b6b9dc12 100644 --- a/src/workload-orchestration/azext_workload_orchestration/common/target.py +++ b/src/workload-orchestration/azext_workload_orchestration/common/target.py @@ -40,6 +40,7 @@ ) from azext_workload_orchestration.common.utils import ( _eprint, + ensure_required_cli_extensions, invoke_cli_command, ) @@ -84,6 +85,17 @@ def target_prepare( step_results = {} + # Step 0: Ensure required az CLI extensions are installed + # (connectedk8s, k8s-extension, customlocation are called via invoke_cli_command + # and would fail with opaque errors if missing) + try: + ensure_required_cli_extensions() + step_results["cli-extensions"] = "Ready" + except Exception as exc: + step_results["cli-extensions"] = f"FAILED: {exc}" + _print_failure_hint(step_results) + raise + try: connected_cluster_id = _preflight_checks(cmd, cluster_name, resource_group) step_results["preflight"] = "Passed" diff --git a/src/workload-orchestration/azext_workload_orchestration/common/utils.py b/src/workload-orchestration/azext_workload_orchestration/common/utils.py index 1a1a5f3785e..e4a602e4463 100644 --- a/src/workload-orchestration/azext_workload_orchestration/common/utils.py +++ b/src/workload-orchestration/azext_workload_orchestration/common/utils.py @@ -156,3 +156,74 @@ def print_step(step_num, total, message, status=""): _eprint(f"{connector} {message} {status}") else: _eprint(f"{connector} {message}...") + + +# --------------------------------------------------------------------------- +# CLI extension dependency check +# --------------------------------------------------------------------------- + +# az CLI extensions that workload-orchestration calls at runtime via +# invoke_cli_command. These MUST be installed before `cluster init` runs, +# otherwise sub-command invocations fail with opaque "command not recognized" +# errors. Mirrors the azext_vme.utils.check_and_add_cli_extension pattern. +REQUIRED_CLI_EXTENSIONS = [ + "connectedk8s", # `connectedk8s show` in _preflight_checks + "k8s-extension", # `k8s-extension list/create` for aio-certmgr + wo-extension + "customlocation", # `customlocation create` for Step 4 +] + + +def check_and_add_cli_extension(extension_name): + """Check if an az CLI extension is installed; install it if missing. + + Uses subprocess (not invoke_cli_command) so that `az extension add` + runs in its own CLI process — needed because in-process invoke does + not pick up newly installed extensions in the same Python process. + + Raises CLIInternalError if the install fails. + """ + import shutil + import subprocess + + az = shutil.which("az") or "az" + + # Check if installed + try: + result = subprocess.run( + [az, "extension", "list", + "--query", f"[?name=='{extension_name}'].name", + "-o", "tsv"], + capture_output=True, text=True, check=True, encoding="utf-8" + ) + if extension_name in (result.stdout or "").strip(): + logger.debug("az cli extension '%s' already installed", extension_name) + return + except subprocess.CalledProcessError as exc: + raise CLIInternalError( + f"Failed to check for az cli extension '{extension_name}': {exc.stderr or exc}" + ) from None + + _eprint(f" ├── Installing required az cli extension: {extension_name}...") + try: + subprocess.run( + [az, "extension", "add", "--name", extension_name, "--yes"], + capture_output=True, text=True, check=True, encoding="utf-8" + ) + _eprint(f" ├── Installed az cli extension: {extension_name} ✓") + except subprocess.CalledProcessError as exc: + raise CLIInternalError( + f"Failed to install required az cli extension '{extension_name}': " + f"{exc.stderr or exc}\n" + f"Please install manually: az extension add --name {extension_name}" + ) from None + + +def ensure_required_cli_extensions(extension_names=None): + """Ensure all required az CLI extensions are installed. + + Defaults to REQUIRED_CLI_EXTENSIONS. Idempotent — skips already-installed + extensions. Fails fast with a clear message if any install fails. + """ + extensions = extension_names if extension_names is not None else REQUIRED_CLI_EXTENSIONS + for ext in extensions: + check_and_add_cli_extension(ext) From 6e6004223ad1fbce881ec7ecc06ea29bcc10c35e Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 09:39:40 +0530 Subject: [PATCH 2/7] [workload-orchestration] context capability-add: drop noisy "Adding N: ..." log The "Adding N: name1, name2, ..." line was duplicating information the user just typed and could dump a huge comma-separated list to stdout for bulk capability-add operations. The subsequent "Done (N total capabilities)" line already conveys the result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azext_workload_orchestration/common/context.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/workload-orchestration/azext_workload_orchestration/common/context.py b/src/workload-orchestration/azext_workload_orchestration/common/context.py index 07e716bf816..513a571d512 100644 --- a/src/workload-orchestration/azext_workload_orchestration/common/context.py +++ b/src/workload-orchestration/azext_workload_orchestration/common/context.py @@ -288,8 +288,6 @@ def capability_add(cli_ctx, resource_group, context_name, name=None, return ctx merged = existing + added - names_str = ", ".join(c["name"] for c in added) - _log(f"Adding {len(added)}: {names_str}") updated = _patch_context_capabilities( cli_ctx, sub_id, resource_group, context_name, merged From ba68de93d575f64c380b16f6e351864cf20cd62a Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 10:27:24 +0530 Subject: [PATCH 3/7] [workload-orchestration] Bump 5.2.1 + docs for CLI extension auto-install Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/workload-orchestration/HISTORY.rst | 5 +++++ src/workload-orchestration/README.md | 25 +++++++++++++++++++++++++ src/workload-orchestration/setup.py | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/workload-orchestration/HISTORY.rst b/src/workload-orchestration/HISTORY.rst index c28b835c019..b1016e44d9d 100644 --- a/src/workload-orchestration/HISTORY.rst +++ b/src/workload-orchestration/HISTORY.rst @@ -2,6 +2,11 @@ Release History =============== +5.2.1 +++++++ +* **Auto-install of required az CLI dependencies** — ``az workload-orchestration cluster init`` (and any flow that invokes ``target_prepare``) now performs a pre-flight check for the ``connectedk8s``, ``k8s-extension``, and ``customlocation`` az CLI extensions, and installs any that are missing. Previously, missing dependencies surfaced as opaque ``command not recognized`` errors deep inside the cluster onboarding flow. +* **Cleaner output for ``az workload-orchestration context capability-add``** — removed redundant ``Adding N: `` log line that duplicated information already shown in the ``✓ Done (N total capabilities)`` summary. + 5.2.0 ++++++ * **CLI Onboarding Simplification** — reduces onboarding from 11 commands to 4: diff --git a/src/workload-orchestration/README.md b/src/workload-orchestration/README.md index 7ea785faf35..3121ad9302a 100644 --- a/src/workload-orchestration/README.md +++ b/src/workload-orchestration/README.md @@ -38,6 +38,31 @@ All workload orchestration resources are managed through Azure Resource Manager, This guide will help you get started with Workload Orchestration for authoring, deploying, and monitoring application configurations using the converged object model. +### Prerequisites + +Workload Orchestration relies on three other Azure CLI extensions at runtime: + +| Extension | Used for | +|---|---| +| `connectedk8s` | Querying the Arc-connected cluster during pre-flight checks | +| `k8s-extension` | Installing the `aio-certmgr` and `microsoft.workloadorchestration` (`tco`) cluster extensions | +| `customlocation` | Creating the Custom Location that targets reference | + +**You do not need to install these manually.** Starting in `5.2.1`, `az workload-orchestration cluster init` (and any command that calls `target_prepare`) automatically detects missing extensions and installs them before continuing. If an install fails (for example due to network or permission issues), the command stops with a clear message and the manual install command, e.g.: + +```text +ERROR: Failed to install required az cli extension 'connectedk8s': + Please install manually: az extension add --name connectedk8s +``` + +To install all three up front yourself: + +```bash +az extension add --name connectedk8s +az extension add --name k8s-extension +az extension add --name customlocation +``` + Key features of the public preview release include end-to-end flows for application dependencies, along with an enhanced UI experience offering additional capabilities like Compare, Copy, Delete, Uninstall, and more. --- diff --git a/src/workload-orchestration/setup.py b/src/workload-orchestration/setup.py index 67a7a051800..23700de8b72 100644 --- a/src/workload-orchestration/setup.py +++ b/src/workload-orchestration/setup.py @@ -10,7 +10,7 @@ # HISTORY.rst entry. -VERSION = '5.2.0' +VERSION = '5.2.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 2c6c358cb97d5f8d322bec44776c5116c9e70e76 Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 10:28:53 +0530 Subject: [PATCH 4/7] [workload-orchestration] Revert README changes (keep HISTORY only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/workload-orchestration/README.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/workload-orchestration/README.md b/src/workload-orchestration/README.md index 3121ad9302a..7ea785faf35 100644 --- a/src/workload-orchestration/README.md +++ b/src/workload-orchestration/README.md @@ -38,31 +38,6 @@ All workload orchestration resources are managed through Azure Resource Manager, This guide will help you get started with Workload Orchestration for authoring, deploying, and monitoring application configurations using the converged object model. -### Prerequisites - -Workload Orchestration relies on three other Azure CLI extensions at runtime: - -| Extension | Used for | -|---|---| -| `connectedk8s` | Querying the Arc-connected cluster during pre-flight checks | -| `k8s-extension` | Installing the `aio-certmgr` and `microsoft.workloadorchestration` (`tco`) cluster extensions | -| `customlocation` | Creating the Custom Location that targets reference | - -**You do not need to install these manually.** Starting in `5.2.1`, `az workload-orchestration cluster init` (and any command that calls `target_prepare`) automatically detects missing extensions and installs them before continuing. If an install fails (for example due to network or permission issues), the command stops with a clear message and the manual install command, e.g.: - -```text -ERROR: Failed to install required az cli extension 'connectedk8s': - Please install manually: az extension add --name connectedk8s -``` - -To install all three up front yourself: - -```bash -az extension add --name connectedk8s -az extension add --name k8s-extension -az extension add --name customlocation -``` - Key features of the public preview release include end-to-end flows for application dependencies, along with an enhanced UI experience offering additional capabilities like Compare, Copy, Delete, Uninstall, and more. --- From 99b833e882194286b9139543990e9d720a766ce8 Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 11:15:24 +0530 Subject: [PATCH 5/7] hierarchy create: show ServiceGroup with tick in tree output Add ServiceGroup creation status line (created/reused) with checkmark in hierarchy create tree output, consistent with Site, Configuration, and ConfigurationReference lines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azext_workload_orchestration/common/hierarchy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/workload-orchestration/azext_workload_orchestration/common/hierarchy.py b/src/workload-orchestration/azext_workload_orchestration/common/hierarchy.py index 30d6c312ce4..a20dd3197a0 100644 --- a/src/workload-orchestration/azext_workload_orchestration/common/hierarchy.py +++ b/src/workload-orchestration/azext_workload_orchestration/common/hierarchy.py @@ -277,8 +277,10 @@ def _create_sg_level( # pylint: disable=too-many-arguments # 1. Create ServiceGroup _eprint(f"{parent_prefix}{connector}{name} ({level})") + sg_url = f"{get_arm_endpoint(cmd)}{sg_id}" + sg_existed = _arm_get(cmd, sg_url, SERVICE_GROUP_API_VERSION) is not None try: - _arm_put(cmd, f"{get_arm_endpoint(cmd)}{sg_id}", { + _arm_put(cmd, sg_url, { "properties": { "displayName": name, "parent": {"resourceId": parent_id}, @@ -359,9 +361,11 @@ def _create_sg_level( # pylint: disable=too-many-arguments children = node.get("children") has_children = children is not None + sg_label = "(reused) " if sg_existed else "(created) " site_label = "(reused) " if existing_sg_site else "" config_label = "(reused) " if config_reused else "" ref_label = "(reused) " if existing_ref else "" + _eprint(f"{child_prefix}├── ServiceGroup '{name}' {sg_label}✓") _eprint(f"{child_prefix}├── Site '{effective_site_name}' {site_label}✓") _eprint(f"{child_prefix}├── Configuration '{config_name}' {config_label}✓") if has_children: From a3657940ab2d45f24bec1d9ac913ba569942f772 Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 13:22:34 +0530 Subject: [PATCH 6/7] Remove noisy 'Removing N:' log from remove-capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the same cleanup already applied to add-capability. The '✓ Done' message is sufficient feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azext_workload_orchestration/common/context.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/workload-orchestration/azext_workload_orchestration/common/context.py b/src/workload-orchestration/azext_workload_orchestration/common/context.py index 513a571d512..d168943a440 100644 --- a/src/workload-orchestration/azext_workload_orchestration/common/context.py +++ b/src/workload-orchestration/azext_workload_orchestration/common/context.py @@ -342,9 +342,6 @@ def capability_remove(cli_ctx, resource_group, context_name, name=None, "Use --yes to confirm removal in non-interactive sessions." ) from exc - names_str = ", ".join(c["name"] for c in to_remove) - _log(f"Removing {len(to_remove)}: {names_str}") - updated = _patch_context_capabilities( cli_ctx, sub_id, resource_group, context_name, remaining ) From 6439d64c7be6c4e2722c66093434058b5ad22195 Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 13 May 2026 13:24:34 +0530 Subject: [PATCH 7/7] =?UTF-8?q?Remove=20'=E2=9C=93=20Done'=20log=20from=20?= =?UTF-8?q?add/remove-capability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both add-capability and remove-capability were printing noisy '✓ Done (N total capabilities)' messages to stderr. The JSON response already contains the updated resource — no need for extra status noise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azext_workload_orchestration/common/context.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/workload-orchestration/azext_workload_orchestration/common/context.py b/src/workload-orchestration/azext_workload_orchestration/common/context.py index d168943a440..2c541ce39c3 100644 --- a/src/workload-orchestration/azext_workload_orchestration/common/context.py +++ b/src/workload-orchestration/azext_workload_orchestration/common/context.py @@ -292,7 +292,6 @@ def capability_add(cli_ctx, resource_group, context_name, name=None, updated = _patch_context_capabilities( cli_ctx, sub_id, resource_group, context_name, merged ) - _log(f"\u2713 Done ({len(merged)} total capabilities)") return updated @@ -345,7 +344,6 @@ def capability_remove(cli_ctx, resource_group, context_name, name=None, updated = _patch_context_capabilities( cli_ctx, sub_id, resource_group, context_name, remaining ) - _log(f"\u2713 Done ({len(remaining)} total capabilities)") return updated