Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
139d152
feat: enhanced target create with onboarding simplification flags
Apr 14, 2026
5d9fb5a
refactor: extract onboarding handlers into focused modules
Apr 14, 2026
5374406
fix: resolve context_id before init-hierarchy and fix sub_id lookup
Apr 14, 2026
cc808d4
refactor: remove standalone 'target prepare' command
Apr 14, 2026
be0e419
refactor: remove standalone 'hierarchy create' command
Apr 14, 2026
9b5c319
fix: address PR review comments from Copilot
Apr 14, 2026
bc354b4
refactor: align with team feedback - separate target init, remove ini…
Apr 14, 2026
a927395
feat: add target deploy command (review → publish → install)
Apr 14, 2026
3bb524c
feat: enhance target deploy with friendly name, resume-from, config-set
Apr 14, 2026
d88dc65
fix: extract solutionVersionId from properties.id in LRO response
Apr 14, 2026
3ca0b77
fix: add --solution flag to config-set and improve logging
Apr 14, 2026
066689c
refactor: remove skip flags, fix linter errors, add short aliases
Apr 14, 2026
e5e330c
cleanup: remove duplicate output from target init
Apr 14, 2026
84a10dc
cleanup: remove preview tags from target and support command groups
Apr 14, 2026
d81f4bc
fix: step counter display in target deploy
Apr 14, 2026
230ea5c
fix: return install LRO result matching native target install format
Apr 14, 2026
fa2bc60
cleanup: remove diagnostic summary from target init success output
Apr 15, 2026
51e988e
feat: context create accepts --site-id to auto-create site reference
Apr 15, 2026
45ad448
refactor: remove --resume-from and --solution-version-id from target …
Apr 15, 2026
bca3b13
feat: merge deploy into target install, remove standalone deploy command
Apr 15, 2026
c5e06bb
refactor: remove config-template args, auto-derive from solution temp…
Apr 15, 2026
ba7bd26
refactor: rename 'target init' to 'cluster init'
Apr 15, 2026
d98e2aa
refactor: remove default target-specification auto-injection
Apr 15, 2026
7250fe9
refactor: remove --solution-template-rg per Shubham's feedback
Apr 15, 2026
24992eb
cleanup: remove short aliases, use full option names only
Apr 15, 2026
7927663
fix: remove leftover solution_template_rg reference in config path
Apr 15, 2026
e600c9a
refactor: remove skip, kube, and reinstall options from cluster init
Apr 16, 2026
4642f2d
feat: add hierarchy create command (ResourceGroup + ServiceGroup)
Apr 16, 2026
e735464
feat: hierarchy create with SG support, RBAC propagation, RG-scoped c…
Apr 16, 2026
5014bb0
refactor: remove --solution-instance-name and --solution-dependencies…
Apr 20, 2026
5f91bc6
fix: resolve all pylint/flake8 lint errors
Apr 20, 2026
3bec477
chore: bump version to 5.2.0, update HISTORY.rst
Apr 20, 2026
288136b
fix: linter errors - add group help, fix example, add short aliases
Apr 20, 2026
bc14ab8
fix: handle @file.yaml in hierarchy-spec parser (P1 bug)
Apr 20, 2026
688c9bf
feat: add -l alias for --configuration-location on hierarchy create
Apr 21, 2026
40242c8
fix: move all progress output to stderr for clean -o json/table/tsv
Apr 21, 2026
705d480
feat: tree-style hierarchy output, remove RBAC waiting message
Apr 21, 2026
09c270e
cleanup: remove folder emoji from hierarchy output
Apr 21, 2026
4be5606
fix: tree output, add --solution-template-rg, clean deploy output
Apr 21, 2026
f534322
fix: target create service-group messages to stderr
Apr 21, 2026
3352fea
style: use tree characters (├── └──) and ✓ for all command output
Apr 21, 2026
ed0afe4
clean: remove all recommendation/mitigation text from errors
Apr 21, 2026
e6e2407
fix: reduce RBAC wait to 3 retries (30s max)
Apr 21, 2026
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
20 changes: 20 additions & 0 deletions src/workload-orchestration/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

Release History
===============
5.2.0
++++++
* **CLI Onboarding Simplification** — reduces onboarding from 11 commands to 4:
* Added ``az workload-orchestration cluster init`` command:
* Prepares Arc-connected clusters for WO (cert-manager, trust-manager, extension, custom location)
* Idempotent — safely skips components already installed
* Supports ``--release-train``, ``--extension-version``, ``--custom-location-name``
* Added ``az workload-orchestration hierarchy create`` command:
* Creates full hierarchy stack (Site + Configuration + ConfigurationReference) in one command
* Supports ResourceGroup (shorthand or YAML) and ServiceGroup (up to 3 levels, recursive)
* Handles RBAC propagation waits for ServiceGroup hierarchies
* Enhanced ``az workload-orchestration context create``:
* Added ``--site-id`` argument to auto-create site-reference after context creation
* Enhanced ``az workload-orchestration target create``:
* Added ``--service-group`` argument to auto-link target to a Service Group after creation
* Enhanced ``az workload-orchestration target install``:
* Added ``--solution-template-version-id``, ``--solution-template-name``, ``--solution-template-version`` for full deploy chain (review → publish → install)
* Added ``--configuration`` to set config values before review (auto-derives config template args)
* Existing ``--solution-version-id`` direct install flow unchanged

5.1.1
++++++
* Resolved solution template name to uniqueIdentifier for ``az workload-orchestration target solution-revision-list`` and ``az workload-orchestration target solution-instance-list``
Expand Down
59 changes: 59 additions & 0 deletions src/workload-orchestration/azext_workload_orchestration/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,62 @@
- name: Use a specific kubeconfig and context
text: az workload-orchestration support create-bundle --kube-config ~/.kube/prod-config --kube-context my-cluster
"""

helps['workload-orchestration cluster init'] = """
type: command
short-summary: Prepare an Arc-connected Kubernetes cluster for Workload Orchestration.
long-summary: |
Installs all prerequisites on an Arc-connected cluster to make it ready for
Workload Orchestration. This is an idempotent operation that skips components
already installed.

Steps performed:
1. Verify cluster is Arc-connected with required features enabled
2. Install cert-manager (if not present)
3. Install trust-manager (if not present)
4. Install WO extension (if not present)
5. Create custom location (if not present)

After running this command, use the output custom location ID with
'az workload-orchestration target create --extended-location'.
examples:
- name: Initialize a cluster with defaults
text: az workload-orchestration cluster init -c my-cluster -g my-rg -l eastus2euap
- name: Initialize with a specific release train
text: az workload-orchestration cluster init -c my-cluster -g my-rg -l eastus2euap --release-train dev
- name: Pin a specific extension version
text: az workload-orchestration cluster init -c my-cluster -g my-rg -l eastus2euap --extension-version 2.1.28
- name: Custom location name
text: az workload-orchestration cluster init -c my-cluster -g my-rg -l eastus2euap --custom-location-name my-cl
"""

helps['workload-orchestration hierarchy create'] = """
type: command
short-summary: Create a hierarchy (Site + Configuration + ConfigurationReference) in one command.
long-summary: |
Creates the full resource stack for a hierarchy level:
1. Site (with level label)
2. Configuration (in specified region)
3. ConfigurationReference (links site to configuration)

Supports two types:
- ResourceGroup (default): single site in a resource group
- ServiceGroup: nested sites under a service group (up to 3 levels)
examples:
- name: Create RG hierarchy from YAML file
text: az workload-orchestration hierarchy create -g my-rg --configuration-location eastus2euap --hierarchy-spec "@hierarchy.yaml"
- name: Create RG hierarchy with shorthand
text: az workload-orchestration hierarchy create -g my-rg --configuration-location eastus2euap --hierarchy-spec "name=Mehoopany level=factory"
- name: Create ServiceGroup hierarchy from YAML
text: az workload-orchestration hierarchy create -g my-rg --configuration-location eastus2euap --hierarchy-spec "@sg-hierarchy.yaml"
"""

helps['workload-orchestration cluster'] = """
type: group
short-summary: Commands for cluster preparation for workload orchestration.
"""

helps['workload-orchestration hierarchy'] = """
type: group
short-summary: Commands for managing workload orchestration hierarchies.
"""
79 changes: 79 additions & 0 deletions src/workload-orchestration/azext_workload_orchestration/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,82 @@ def load_arguments(self, _): # pylint: disable=unused-argument
options_list=['--kube-context'],
help='Kubernetes context to use. Defaults to current context.',
)
c.argument(
'skip_site_reference',
options_list=['--skip-site-reference'],
action='store_true',
help='Skip auto-creation of site-reference to context.',
)
Comment on lines +68 to +73
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The new skip_site_reference argument is registered under the workload-orchestration support create-bundle command context, but it is unrelated to support bundles and is not consumed by that command. This will expose a confusing/unused flag to users; if the intent is to control site-reference creation it should be registered under the relevant context/onboarding command (or removed).

Suggested change
c.argument(
'skip_site_reference',
options_list=['--skip-site-reference'],
action='store_true',
help='Skip auto-creation of site-reference to context.',
)

Copilot uses AI. Check for mistakes.

with self.argument_context('workload-orchestration cluster init') as c:
c.argument('cluster_name', options_list=['--cluster-name', '-c'],
help='Name of the Arc-connected Kubernetes cluster.', required=True)
c.argument('resource_group', options_list=['--resource-group', '-g'],
help='Resource group of the Arc-connected cluster.', required=True)
c.argument('location', options_list=['--location', '-l'],
help='Azure region for the custom location (e.g., eastus2euap).', required=True)
c.argument('release_train', options_list=['--release-train'],
help='Extension release train. Default: stable.')
c.argument('extension_version', options_list=['--extension-version'],
help='Specific WO extension version to install.')
c.argument('extension_name', options_list=['--extension-name'],
help='Name for the WO extension resource. Default: wo-extension.')
c.argument('custom_location_name', options_list=['--custom-location-name'],
help='Name for the custom location. Default: `<cluster-name>-cl`.')

with self.argument_context('workload-orchestration hierarchy create') as c:
c.argument('resource_group', options_list=['--resource-group', '-g'],
help='Resource group for Configuration resources.', required=True)
c.argument('configuration_location', options_list=['--configuration-location', '-l'],
help='Azure region for the Configuration resource (e.g., eastus2euap).', required=True)
c.argument('hierarchy_spec', options_list=['--hierarchy-spec'],
help='Hierarchy specification as YAML/JSON file (@file.yaml) or shorthand syntax.',
required=True, type=_parse_hierarchy_spec)


def _parse_hierarchy_spec(value):
"""Parse hierarchy spec from file path, YAML content, or shorthand syntax.

Handles three input modes:
1. File path: 'hierarchy.yaml' or '@hierarchy.yaml' (@ stripped by CLI)
2. YAML content: when CLI framework pre-loads @file, we get raw YAML text
3. Shorthand: 'name=X level=Y type=Z'
"""
import os

# Mode 1: File path (with or without @)
filepath = value.lstrip('@')
if os.path.exists(filepath):
try:
import yaml
except ImportError:
import json
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
with open(filepath, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)

# Mode 2: YAML content (CLI framework pre-loaded @file)
# Detect YAML by checking for colon-separated key-value or newlines
if ':' in value and ('\n' in value or 'name:' in value or 'level:' in value):
try:
import yaml
parsed = yaml.safe_load(value)
if isinstance(parsed, dict):
return parsed
except Exception:
pass

# Mode 3: Shorthand syntax: name=X level=Y type=Z
result = {}
for pair in value.split():
if '=' in pair:
k, v = pair.split('=', 1)
result[k] = v
if not result:
from azure.cli.core.azclierror import ValidationError
raise ValidationError(
f"Invalid hierarchy-spec: '{value}'. "
"Use a YAML file path or shorthand: name=X level=Y"
)
return result
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# flake8: noqa

from azure.cli.core.aaz import *
from azure.cli.core.azclierror import CLIInternalError as CLIError


@register_command(
Expand Down Expand Up @@ -128,6 +129,14 @@ def _build_arguments_schema(cls, *args, **kwargs):

tags = cls._args_schema.tags
tags.Element = AAZStrArg()

# Custom arg: --site-id (not sent to ARM, used in post_operations)
_args_schema.site_id = AAZStrArg(
options=["--site-id"],
arg_group="Onboarding",
help="ARM resource ID of a Site to auto-create a site reference after context creation.",
)

return cls._args_schema

def _execute_operations(self):
Expand All @@ -141,7 +150,47 @@ def pre_operations(self):

@register_callback
def post_operations(self):
pass
if hasattr(self.ctx.args, 'site_id') and self.ctx.args.site_id:
self._create_site_reference()

def _create_site_reference(self):
"""Auto-create a site reference linking the site to this context."""
import logging
import re
logger = logging.getLogger(__name__)

site_id = str(self.ctx.args.site_id)
context_name = str(self.ctx.args.context_name)
rg = str(self.ctx.args.resource_group)

# Extract site name from ARM ID for the reference name
site_name = site_id.rstrip("/").split("/")[-1]
ref_name = f"{site_name}-ref"
# Sanitize: only alphanumeric and hyphens, 3-61 chars
ref_name = re.sub(r'[^a-zA-Z0-9-]', '-', ref_name)[:61]

logger.info("Creating site reference '%s' -> %s", ref_name, site_id)

try:
from azext_workload_orchestration.onboarding.utils import invoke_cli_command, CmdProxy
cmd_proxy = CmdProxy(self.ctx.cli_ctx)
invoke_cli_command(cmd_proxy, [
"workload-orchestration", "context", "site-reference", "create",
"-g", rg,
"--context-name", context_name,
"--site-reference-name", ref_name,
"--site-id", site_id,
])
logger.info("Site reference '%s' created successfully", ref_name)
except Exception as exc:
logger.warning("Site reference creation failed: %s", exc)
raise CLIError(
f"Context created successfully, but site reference creation failed: {exc}\n"
f"Run manually:\n"
f" az workload-orchestration context site-reference create "
f"-g {rg} --context-name {context_name} "
f"--site-reference-name {ref_name} --site-id {site_id}"
)

def _output(self, *args, **kwargs):
result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

logger = logging.getLogger(__name__)


@register_command(
"workload-orchestration target create",
)
Expand Down Expand Up @@ -117,10 +118,16 @@ def _build_arguments_schema(cls, *args, **kwargs):
options=["--target-specification"],
arg_group="Properties",
help="Specifies that we are using Helm charts for the k8s deployment",
required=True,

)

# Onboarding simplification arguments
_args_schema.service_group = AAZStrArg(
options=["--service-group"],
arg_group="Onboarding",
help="ServiceGroup name to auto-link this target to after creation.",
)

capabilities = cls._args_schema.capabilities
capabilities.Element = AAZStrArg()

Expand Down Expand Up @@ -170,30 +177,64 @@ def _execute_operations(self):

@register_callback
def pre_operations(self):
# If context_id is not provided, try to get it from config
# Resolve context_id from CLI config if not provided
if not self.ctx.args.context_id:
try:
# Attempt to retrieve the context_id from the config file
context_id = self.ctx.cli_ctx.config.get('workload_orchestration', 'context_id')
if context_id:
self.ctx.args.context_id = context_id
else:
# This else block handles the case where the section exists, but the key is empty
raise CLIInternalError(
"No context-id was provided, and no default context is set. "
"Please provide the --context-id argument or set a default context using 'az workload-orchestration context use'."
)
except configparser.NoSectionError as e:
logger.debug("Config section 'workload_orchestration' not found: %s", e)
# This is the fix: catch the specific error when the [workload_orchestration] section is missing
self._resolve_context_id_from_config()

def _resolve_context_id_from_config(self):
"""Resolve context_id from CLI config if not already set."""
try:
context_id = self.ctx.cli_ctx.config.get('workload_orchestration', 'context_id')
if context_id:
self.ctx.args.context_id = context_id
else:
raise CLIInternalError(
"No context-id was provided, and no default context is set. "
"Please provide the --context-id argument or set a default context using 'az workload-orchestration context use'."
"Please provide the --context-id argument "
"or set a default context using 'az workload-orchestration context use'."
)
except configparser.NoSectionError as e:
logger.debug("Config section 'workload_orchestration' not found: %s", e)
raise CLIInternalError(
"No context-id was provided, and no default context is set. "
"Please provide the --context-id argument "
"or set a default context using 'az workload-orchestration context use'."
)

@register_callback
def post_operations(self):
pass
# --service-group: auto-link target to SG after creation
if hasattr(self.ctx.args, 'service_group') and self.ctx.args.service_group:
self._handle_service_group_link()

def _handle_service_group_link(self):
"""Link the created target to a service group."""
from azext_workload_orchestration.onboarding.target_sg_link import (
link_target_to_service_group
)
from azext_workload_orchestration.onboarding.utils import CmdProxy
sg_name = str(self.ctx.args.service_group)
# Get target ID from the response
target_id = None
if hasattr(self.ctx.vars, 'instance') and self.ctx.vars.instance:
target_id = self.ctx.vars.instance.get("id")

if not target_id:
# Construct it
sub_id = self.ctx.subscription_id
rg = str(self.ctx.args.resource_group)
name = str(self.ctx.args.target_name)
target_id = f"/subscriptions/{sub_id}/resourceGroups/{rg}/providers/Microsoft.Edge/targets/{name}"

import sys
print(f"└── service-group Linking to '{sg_name}'...", file=sys.stderr)
try:
cmd_proxy = CmdProxy(self.ctx.cli_ctx)
link_target_to_service_group(cmd_proxy, target_id, sg_name)
print(f"└── service-group Linked ✓", file=sys.stderr)
except Exception as exc:
logger.warning("Service group link failed (non-critical): %s", exc)
print(f"└── service-group Link failed (non-critical): {exc}", file=sys.stderr)

def _output(self, *args, **kwargs):
result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True)
Expand Down
Loading
Loading