Skip to content
Draft
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
6 changes: 5 additions & 1 deletion src/fleet/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,8 @@ Release History

1.9.0
++++++
* Add 2026-02-01-preview API Version with UpdateRun MaxConcurrency support. Add fix for ControlPlaneOnly upgrade type requiring no node image selection.
* Add 2026-02-01-preview API Version with UpdateRun MaxConcurrency support. Add fix for ControlPlaneOnly upgrade type requiring no node image selection.

2.0.0
++++++
* Add 2026-03-02-preview API version with ClusterMesh support.
4 changes: 2 additions & 2 deletions src/fleet/azext_fleet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# --------------------------------------------------------------------------------------------

from azure.cli.core import AzCommandsLoader
from azure.cli.core.profiles import register_resource_type, SDKProfile
from azure.cli.core.profiles import register_resource_type

# pylint: disable=unused-import
from azext_fleet._help import helps
Expand All @@ -15,7 +15,7 @@ def register_fleet_resource_type():
register_resource_type(
"latest",
CUSTOM_MGMT_FLEET,
SDKProfile("2026-02-01-preview"),
None,
)


Expand Down
16 changes: 15 additions & 1 deletion src/fleet/azext_fleet/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@
CUSTOM_MGMT_FLEET = CustomResourceType('azext_fleet.vendored_sdks', 'ContainerServiceFleetMgmtClient')


# TODO: Remove this override once the API version is rolled out to production ARM.
# During preview, this API version is only available in the centraluseuap environment.
FLEET_BASE_URL = "https://centraluseuap.management.azure.com"


# container service clients
def get_container_service_client(cli_ctx, subscription_id=None):
return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_FLEET, subscription_id=subscription_id)
return get_mgmt_service_client(
cli_ctx, CUSTOM_MGMT_FLEET,
subscription_id=subscription_id,
base_url_bound=False,
base_url=FLEET_BASE_URL
)


def cf_fleets(cli_ctx, *_):
Expand Down Expand Up @@ -56,6 +66,10 @@ def cf_auto_upgrade_profile_operations(cli_ctx, *_):
return get_container_service_client(cli_ctx).auto_upgrade_profile_operations


def cf_cluster_mesh_profiles(cli_ctx, *_):
return get_container_service_client(cli_ctx).cluster_mesh_profiles


def get_provider_client(cli_ctx):
return get_mgmt_service_client(
cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
Expand Down
68 changes: 68 additions & 0 deletions src/fleet/azext_fleet/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@
examples:
- name: List all members for a given fleet.
text: az fleet member list -g MyFleetResourceGroup -f MyFleetName
- name: List members filtered by a cluster mesh profile.
text: az fleet member list -g MyFleetResourceGroup -f MyFleetName --cluster-mesh-profile MyClusterMeshProfile
"""

helps['fleet member show'] = """
Expand Down Expand Up @@ -584,3 +586,69 @@
- name: Save kubeconfig to a specific file.
text: az fleet namespace get-credentials -g MyFleetResourceGroup -f MyFleetName -n MyManagedNamespace --file ~/my-namespace-config
"""

helps['fleet clustermeshprofile'] = """
type: group
short-summary: Commands to manage cluster mesh profiles.
"""

helps['fleet clustermeshprofile create'] = """
type: command
short-summary: Creates or updates a cluster mesh profile.
parameters:
- name: --member-selector --selector -s
type: string
short-summary: "Kubernetes-style label selector for selecting Fleet members, e.g. 'env=production'."
examples:
- name: Create a cluster mesh profile with a label selector.
text: az fleet clustermeshprofile create -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile --selector "env=production"
- name: Create a cluster mesh profile without a selector (no members selected initially).
text: az fleet clustermeshprofile create -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile
"""

helps['fleet clustermeshprofile show'] = """
type: command
short-summary: Gets a cluster mesh profile.
examples:
- name: Show the details of a cluster mesh profile.
text: az fleet clustermeshprofile show -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile
"""

helps['fleet clustermeshprofile list'] = """
type: command
short-summary: Lists all cluster mesh profiles for a fleet.
examples:
- name: List all cluster mesh profiles for a given fleet.
text: az fleet clustermeshprofile list -g MyFleetResourceGroup -f MyFleetName
"""

helps['fleet clustermeshprofile delete'] = """
type: command
short-summary: Deletes a cluster mesh profile. All members must be removed from the cluster mesh profile before it can be deleted.
examples:
- name: Delete a specific cluster mesh profile.
text: az fleet clustermeshprofile delete -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile
"""

helps['fleet clustermeshprofile apply'] = """
type: command
short-summary: Applies the cluster mesh profile to selected fleet members.
examples:
- name: Apply a cluster mesh profile.
text: az fleet clustermeshprofile apply -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile
- name: Preview what changes would be made without actually applying.
text: az fleet clustermeshprofile apply -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile --what-if --output table
"""

helps['fleet clustermeshprofile list-members'] = """
type: command
short-summary: Lists fleet members for a cluster mesh profile.
long-summary: |
Without --selector, lists members currently applied to the mesh profile.
With --selector, lists members that would match the profile's label selector (i.e. candidates for the next apply).
examples:
- name: List members currently applied to the mesh.
text: az fleet clustermeshprofile list-members -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile
- name: List members that would match the profile's selector.
text: az fleet clustermeshprofile list-members -g MyFleetResourceGroup -f MyFleetName -n MyClusterMeshProfile --selector
"""
21 changes: 21 additions & 0 deletions src/fleet/azext_fleet/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ def load_arguments(self, _):
help='Space-separated labels in key=value format. Example: env=production region=us-west team=devops'
)

with self.argument_context('fleet member list') as c:
c.argument('cluster_mesh_profile', options_list=['--cluster-mesh-profile'],
help='Filter members by cluster mesh profile name.')

with self.argument_context('fleet updaterun') as c:
c.argument('name', options_list=['--name', '-n'], help='Specify name for the update run.')
c.argument('fleet_name', options_list=['--fleet-name', '-f'], help='Specify the fleet name.')
Expand Down Expand Up @@ -207,3 +211,20 @@ def load_arguments(self, _):
c.argument('overwrite_existing', help='Overwrite any existing cluster entry with the same name.')
c.argument('path', options_list=['--file'], type=file_type, completer=FilesCompleter(),
default=os.path.join(os.path.expanduser('~'), '.kube', 'config'))

with self.argument_context('fleet clustermeshprofile') as c:
c.argument('name', options_list=['--name', '-n'], help='Specify name for the cluster mesh profile.')
c.argument('fleet_name', options_list=['--fleet-name', '-f'], help='Specify the fleet name.')

with self.argument_context('fleet clustermeshprofile create') as c:
c.argument('member_selector', options_list=['--member-selector', '--selector', '-s'],
help='Kubernetes-style label selector for selecting Fleet members, e.g. "env=production".')

with self.argument_context('fleet clustermeshprofile apply') as c:
c.argument('what_if', action='store_true', options_list=['--what-if'],
help='Show what changes would be made by the apply operation without actually performing it.')

with self.argument_context('fleet clustermeshprofile list-members') as c:
c.argument('selector', action='store_true', options_list=['--selector'],
help='Filter by the profile\'s label selector (members that would match after apply) '
'instead of currently applied members.')
18 changes: 17 additions & 1 deletion src/fleet/azext_fleet/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
cf_auto_upgrade_profiles,
cf_auto_upgrade_profile_operations,
cf_gates,
cf_fleet_managed_namespaces
cf_fleet_managed_namespaces,
cf_cluster_mesh_profiles
)


Expand Down Expand Up @@ -67,6 +68,12 @@ def load_command_table(self, _):
client_factory=cf_gates
)

cluster_mesh_profiles_sdk = CliCommandType(
operations_tmpl="azext_fleet.vendored_sdks.operations._cluster_mesh_profiles_operations#ClusterMeshProfilesOperations.{}",
operation_group="cluster_mesh_profiles",
client_factory=cf_cluster_mesh_profiles
)

# fleets command group
with self.command_group("fleet", fleets_sdk, client_factory=cf_fleets) as g:
g.custom_command("create", "create_fleet", supports_no_wait=True)
Expand Down Expand Up @@ -135,3 +142,12 @@ def load_command_table(self, _):
g.custom_show_command("show", "show_managed_namespace")
g.custom_command("get-credentials", "get_namespace_credentials")
g.wait_command("wait")

# cluster mesh profiles command group
with self.command_group("fleet clustermeshprofile", cluster_mesh_profiles_sdk, client_factory=cf_cluster_mesh_profiles, is_preview=True) as g:
g.custom_command("create", "create_cluster_mesh_profile", supports_no_wait=True)
g.custom_show_command("show", "show_cluster_mesh_profile")
g.custom_command("list", "list_cluster_mesh_profiles")
g.custom_command("delete", "delete_cluster_mesh_profile", supports_no_wait=True, confirmation=True)
g.custom_command("apply", "apply_cluster_mesh_profile", supports_no_wait=True)
g.custom_command("list-members", "list_cluster_mesh_profile_members")
162 changes: 158 additions & 4 deletions src/fleet/azext_fleet/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
from azext_fleet.constants import SUPPORTED_GATE_STATES_FILTERS
from azext_fleet.constants import SUPPORTED_GATE_STATES_PATCH
from azext_fleet.constants import FLEET_1P_APP_ID
from azext_fleet.vendored_sdks.v2026_02_01_preview.models import (
from azext_fleet.vendored_sdks.v2026_03_02_preview.models import (
PropagationPolicy,
PlacementProfile,
PlacementV1ClusterResourcePlacementSpec,
PlacementV1PlacementPolicy,
PropagationType,
PlacementType
PlacementType,
)

logger = get_logger(__name__)
Expand Down Expand Up @@ -381,8 +381,12 @@ def update_fleet_member(cmd,
def list_fleet_member(cmd, # pylint: disable=unused-argument
client,
resource_group_name,
fleet_name):
return client.list_by_fleet(resource_group_name, fleet_name)
fleet_name,
cluster_mesh_profile=None):
filter_expr = None
if cluster_mesh_profile:
filter_expr = f"clusterMeshProfile eq {cluster_mesh_profile}"
return client.list_by_fleet(resource_group_name, fleet_name, filter=filter_expr)


def show_fleet_member(cmd, # pylint: disable=unused-argument
Expand Down Expand Up @@ -1066,3 +1070,153 @@ def get_namespace_credentials(cmd,

# Apply kubelogin conversion to the final file after namespace modification
_convert_kubeconfig_to_azurecli(path)


def create_cluster_mesh_profile(cmd,
client,
resource_group_name,
fleet_name,
name,
member_selector=None,
no_wait=False):
cluster_mesh_profile_model = cmd.get_models(
"ClusterMeshProfile",
resource_type=CUSTOM_MGMT_FLEET,
operation_group="cluster_mesh_profiles"
)
cluster_mesh_profile_properties_model = cmd.get_models(
"ClusterMeshProfileProperties",
resource_type=CUSTOM_MGMT_FLEET,
operation_group="cluster_mesh_profiles"
)
member_selector_model = cmd.get_models(
"MemberSelector",
resource_type=CUSTOM_MGMT_FLEET,
operation_group="cluster_mesh_profiles"
)

selector = None
if member_selector is not None:
selector = member_selector_model(by_label=member_selector)

properties = cluster_mesh_profile_properties_model(member_selector=selector)
profile = cluster_mesh_profile_model(properties=properties)

return sdk_no_wait(
no_wait,
client.begin_create_or_update,
resource_group_name,
fleet_name,
name,
profile
)


def show_cluster_mesh_profile(cmd, # pylint: disable=unused-argument
client,
resource_group_name,
fleet_name,
name):
return client.get(resource_group_name, fleet_name, name)


def list_cluster_mesh_profiles(cmd, # pylint: disable=unused-argument
client,
resource_group_name,
fleet_name):
return client.list_by_fleet(resource_group_name, fleet_name)


def delete_cluster_mesh_profile(cmd, # pylint: disable=unused-argument
client,
resource_group_name,
fleet_name,
name,
no_wait=False):
return sdk_no_wait(no_wait, client.begin_delete, resource_group_name, fleet_name, name)


def apply_cluster_mesh_profile(cmd,
client,
resource_group_name,
fleet_name,
name,
what_if=False,
no_wait=False):
if what_if:
return _apply_cluster_mesh_what_if(cmd, resource_group_name, fleet_name, name)

return sdk_no_wait(no_wait, client.begin_apply, resource_group_name, fleet_name, name)


def list_cluster_mesh_profile_members(cmd,
client, # pylint: disable=unused-argument
resource_group_name,
fleet_name,
name,
selector=False):
"""List fleet members for a cluster mesh profile.

Modes:
--name cmp-1 members currently applied to the mesh
(server-side: $filter=clusterMeshProfile eq cmp-1)
--name cmp-1 --selector members matching the profile's label selector
(server-side: $filter=clusterMeshProfile.Selector eq cmp-1)
"""
members_client = cf_fleet_members(cmd.cli_ctx)
if selector:
filter_expr = f"clusterMeshProfile.Selector eq {name}"
else:
filter_expr = f"clusterMeshProfile eq {name}"
return members_client.list_by_fleet(resource_group_name, fleet_name, filter=filter_expr)


def _apply_cluster_mesh_what_if(cmd, resource_group_name, fleet_name, name):
"""Simulate apply by comparing currently-applied members vs selector-matched members."""
members_client = cf_fleet_members(cmd.cli_ctx)

# Members currently in the mesh (already applied)
current_filter = f"clusterMeshProfile eq {name}"
current_members = {
m.name: m for m in members_client.list_by_fleet(
resource_group_name, fleet_name, filter=current_filter
)
}

# Members that match the selector (would be in the mesh after apply)
selector_filter = f"clusterMeshProfile.Selector eq {name}"
desired_members = {
m.name: m for m in members_client.list_by_fleet(
resource_group_name, fleet_name, filter=selector_filter
)
}

results = []
all_names = set(current_members.keys()) | set(desired_members.keys())

for member_name in sorted(all_names):
in_current = member_name in current_members
in_desired = member_name in desired_members

member = desired_members.get(member_name) or current_members.get(member_name)

mesh_state = None
if member.mesh_properties and member.mesh_properties.status:
mesh_state = member.mesh_properties.status.state

if in_desired and not in_current:
action = "Add"
elif in_current and not in_desired:
action = "Remove"
else:
action = "-"

results.append({
"ClusterResourceId": member.cluster_resource_id,
"ETag": member.e_tag,
"Name": member.name,
"Action": action,
"MeshMembershipState": mesh_state or "-"
})

return results
Loading
Loading