Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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
49 changes: 49 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,52 @@
- 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 --configuration-location eastus2euap --hierarchy-spec "@sg-hierarchy.yaml"
"""
62 changes: 62 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,65 @@ 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.',
)

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'],
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 or shorthand syntax."""
import os

# Handle @file syntax (@ may be stripped by CLI framework)
filepath = value.lstrip('@')
if os.path.exists(filepath):
try:
import yaml
except ImportError:
import json as yaml_fallback
with open(filepath, 'r', encoding='utf-8') as f:
return yaml_fallback.load(f)
with open(filepath, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)

# Shorthand: 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(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Is this optionalk should itg be optional if passewd create site reference

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"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Remove this

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,63 @@ 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}"

print(f"[service-group] Linking target to '{sg_name}'...")
try:
cmd_proxy = CmdProxy(self.ctx.cli_ctx)
link_target_to_service_group(cmd_proxy, target_id, sg_name)
print(f"[service-group] Linked [OK]")
except Exception as exc:
logger.warning("Service group link failed (non-critical): %s", exc)
print(f"[service-group] Link failed (non-critical): {exc}")

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