diff --git a/src/fleet/HISTORY.rst b/src/fleet/HISTORY.rst index d6234c27765..1df26217588 100644 --- a/src/fleet/HISTORY.rst +++ b/src/fleet/HISTORY.rst @@ -45,3 +45,13 @@ Release History 0.2.7 ++++++ * Fix for `az fleet updaterun --node-image-selection` argument. + +0.2.8 +++++++ +* Updates to Fleet identity options. + +0.3.0 +++++++ +* Resolved issues related to system & user assigned MSI. +* UpdateRun now takes a Strategy name in lieu of resource Id, e.g., `az fleet updaterun create --update_strategy_name UpdateStrategyName` +* Deletes now require confirmation. \ No newline at end of file diff --git a/src/fleet/azext_fleet/_help.py b/src/fleet/azext_fleet/_help.py index 395143f39b6..8e9ec948b6a 100644 --- a/src/fleet/azext_fleet/_help.py +++ b/src/fleet/azext_fleet/_help.py @@ -16,22 +16,28 @@ type: command short-summary: Creates or updates a Fleet. parameters: - - name: --tags - type: string - short-summary: The tags of the managed cluster. The managed cluster instance and all resources managed by the cloud provider will be tagged. - name: --dns-name-prefix -p type: string short-summary: Prefix for hostnames that are created. If not specified, generate a hostname using the managed cluster and resource group names. + examples: + - name: Create a hubless fleet + text: az fleet create -g MyResourceGroup -l MyLocation -n MyFleetName --tags "TagKey=TagValue" + - name: Create a hubful fleet + text: az fleet create -g MyResourceGroup -l MyLocation -n MyFleetName --enable-hub --tags "TagKey=TagValue" + """ helps['fleet update'] = """ type: command short-summary: Patches a fleet resource. - parameters: - - name: --tags - type: string - short-summary: The tags of the managed cluster. The managed cluster instance and all resources managed by the cloud provider will be tagged. + examples: + - name: Update a Fleet's tags + text: az fleet update -g MyResourceGroup -n MyFleetName --tags Key=Value + - name: Update a Fleet to use a system assigned managed service identity. + text: az fleet update -g MyResourceGroup -n MyFleetName --enable-managed-identity --tags Key=Value + - name: Update a Fleet to use a user assigned managed service identity. + text: az fleet update -g MyResourceGroup -n MyFleetName --enable-managed-identity --assign-identity "/subscription/00000000-0000-0000-0000-000000000000/resourcegroup/MyResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MyIdentity" --tags Key=Value """ helps['fleet show'] = """ diff --git a/src/fleet/azext_fleet/_params.py b/src/fleet/azext_fleet/_params.py index fefaa06d61d..bc38fcb855a 100644 --- a/src/fleet/azext_fleet/_params.py +++ b/src/fleet/azext_fleet/_params.py @@ -9,10 +9,11 @@ tags_type, file_type, get_location_type, - get_enum_type + get_enum_type, + get_three_state_flag ) from azure.cli.core.commands.validators import get_default_location_from_resource_group -from azext_fleet._validators import validate_member_cluster_id, validate_kubernetes_version, validate_apiserver_subnet_id, validate_agent_subnet_id, validate_assign_identity, validate_update_strategy_id +from azext_fleet._validators import validate_member_cluster_id, validate_kubernetes_version, validate_apiserver_subnet_id, validate_agent_subnet_id, validate_assign_identity, validate_update_strategy_name def load_arguments(self, _): @@ -34,7 +35,7 @@ def load_arguments(self, _): with self.argument_context('fleet update') as c: c.argument('tags', tags_type) - c.argument('enable_managed_identity', action='store_true', is_preview=True, help='Enable system assigned managed identity (MSI) on the Fleet resource.') + c.argument('enable_managed_identity', arg_type=get_three_state_flag(), is_preview=True, help='Enable system assigned managed identity (MSI) on the Fleet resource.') c.argument('assign_identity', validator=validate_assign_identity, is_preview=True, help='With --enable-managed-identity, enable user assigned managed identity (MSI) on the Fleet resource. Specify the existing user assigned identity resource.') with self.argument_context('fleet get-credentials') as c: @@ -61,7 +62,7 @@ def load_arguments(self, _): c.argument('kubernetes_version', validator=validate_kubernetes_version) c.argument('node_image_selection', arg_type=get_enum_type(['Latest', 'Consistent']), help='Node Image Selection is an option that lets you choose how your clusters\' nodes are upgraded') c.argument('stages', type=file_type, completer=FilesCompleter(), help='Path to a json file that defines stages to upgrade a fleet. See examples for further reference.') - c.argument('update_strategy_id', validator=validate_update_strategy_id, help='The ID of the update strategy to use for this update run. If not specified, the default update strategy will be used.') + c.argument('update_strategy_name', validator=validate_update_strategy_name, help='The name of the update strategy to use for this update run. If not specified, the default update strategy will be used.') with self.argument_context('fleet updatestrategy') as c: c.argument('name', options_list=['--name', '-n'], help='Specify name for the fleet update strategy.') diff --git a/src/fleet/azext_fleet/_validators.py b/src/fleet/azext_fleet/_validators.py index be12a5ed41e..efeb5533198 100644 --- a/src/fleet/azext_fleet/_validators.py +++ b/src/fleet/azext_fleet/_validators.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import semver +import re from azure.cli.core.azclierror import InvalidArgumentValueError from azure.cli.core.util import CLIError @@ -18,12 +18,12 @@ def validate_member_cluster_id(namespace): def validate_kubernetes_version(namespace): - try: - if namespace.kubernetes_version: - semver.VersionInfo.parse(namespace.kubernetes_version) - except ValueError: - raise InvalidArgumentValueError( - "--kubernetes-version must be set as version x.x.x (eg. 1.2.3)") + if namespace.kubernetes_version: + k8s_release_regex = re.compile(r'^[v|V]?(\d+\.\d+(?:\.\d+)?)$') + found = k8s_release_regex.findall(namespace.kubernetes_version) + if not found: + raise InvalidArgumentValueError( + '--kubernetes-version should be the full version number or alias minor version, such as "1.7.12" or "1.7"') def validate_apiserver_subnet_id(namespace): @@ -34,12 +34,9 @@ def validate_agent_subnet_id(namespace): _validate_subnet_id(namespace.agent_subnet_id, "--agent-subnet-id") -def validate_update_strategy_id(namespace): - if namespace.update_strategy_id is not None: - from msrestazure.tools import is_valid_resource_id - if not is_valid_resource_id(namespace.update_strategy_id): - raise CLIError( - "--update-strategy-id is not a valid Azure resource ID.") +def validate_update_strategy_name(namespace): + if namespace.update_strategy_name is not None and not namespace.update_strategy_name.strip(): + raise CLIError("--update-strategy-name is not a valid name") def _validate_subnet_id(subnet_id, name): diff --git a/src/fleet/azext_fleet/commands.py b/src/fleet/azext_fleet/commands.py index 913a6b6ca1b..7cf88b33222 100644 --- a/src/fleet/azext_fleet/commands.py +++ b/src/fleet/azext_fleet/commands.py @@ -40,7 +40,7 @@ def load_command_table(self, _): g.custom_command("update", "update_fleet", supports_no_wait=True) g.custom_show_command("show", "show_fleet") g.custom_command("list", "list_fleet") - g.custom_command("delete", "delete_fleet", supports_no_wait=True) + g.custom_command("delete", "delete_fleet", supports_no_wait=True, confirmation=True) g.custom_command("get-credentials", "get_credentials") g.wait_command("wait") @@ -48,7 +48,7 @@ def load_command_table(self, _): with self.command_group("fleet member", fleet_members_sdk, client_factory=cf_fleet_members) as g: g.custom_command("create", "create_fleet_member", supports_no_wait=True) g.custom_command("update", "update_fleet_member") - g.custom_command("delete", "delete_fleet_member", supports_no_wait=True) + g.custom_command("delete", "delete_fleet_member", supports_no_wait=True, confirmation=True) g.custom_command("list", "list_fleet_member") g.custom_show_command("show", "show_fleet_member") g.wait_command("wait") @@ -58,7 +58,7 @@ def load_command_table(self, _): g.custom_command("create", "create_update_run", supports_no_wait=True) g.custom_show_command("show", "show_update_run") g.custom_command("list", "list_update_run") - g.custom_command("delete", "delete_update_run", supports_no_wait=True) + g.custom_command("delete", "delete_update_run", supports_no_wait=True, confirmation=True) g.custom_command("start", "start_update_run", supports_no_wait=True) g.custom_command("stop", "stop_update_run", supports_no_wait=True) g.wait_command("wait") @@ -68,5 +68,5 @@ def load_command_table(self, _): g.custom_command("create", "create_fleet_update_strategy", supports_no_wait=True) g.custom_show_command("show", "show_fleet_update_strategy") g.custom_command("list", "list_fleet_update_strategies") - g.custom_command("delete", "delete_fleet_update_strategy", supports_no_wait=True) + g.custom_command("delete", "delete_fleet_update_strategy", supports_no_wait=True, confirmation=True) g.wait_command("wait") diff --git a/src/fleet/azext_fleet/custom.py b/src/fleet/azext_fleet/custom.py index 8d2357c6e68..487b53ec1a2 100644 --- a/src/fleet/azext_fleet/custom.py +++ b/src/fleet/azext_fleet/custom.py @@ -16,6 +16,7 @@ from azext_fleet._helpers import print_or_merge_credentials +# pylint: disable=too-many-locals def create_fleet(cmd, client, resource_group_name, @@ -38,7 +39,9 @@ def create_fleet(cmd, operation_group="fleets" ) + poll_interval = 5 if enable_hub: + poll_interval = 30 fleet_hub_profile_model = cmd.get_models( "FleetHubProfile", resource_type=CUSTOM_MGMT_FLEET, @@ -94,8 +97,13 @@ def create_fleet(cmd, if enable_managed_identity: managed_service_identity.type = "SystemAssigned" if assign_identity is not None: + user_assigned_identity_model = cmd.get_models( + "UserAssignedIdentity", + resource_type=CUSTOM_MGMT_FLEET, + operation_group="fleets" + ) managed_service_identity.type = "UserAssigned" - managed_service_identity.user_assigned_identities = {assign_identity: None} + managed_service_identity.user_assigned_identities = {assign_identity: user_assigned_identity_model()} elif assign_identity is not None: raise CLIError("Cannot assign identity without enabling managed identity.") @@ -106,14 +114,19 @@ def create_fleet(cmd, identity=managed_service_identity ) - return sdk_no_wait(no_wait, client.begin_create_or_update, resource_group_name, name, fleet) + return sdk_no_wait(no_wait, + client.begin_create_or_update, + resource_group_name, + name, + fleet, + polling_interval=poll_interval) def update_fleet(cmd, client, resource_group_name, name, - enable_managed_identity=False, + enable_managed_identity=None, assign_identity=None, tags=None, no_wait=False): @@ -128,20 +141,29 @@ def update_fleet(cmd, operation_group="fleets" ) - managed_service_identity = fleet_managed_service_identity_model(type="None") - if enable_managed_identity: - managed_service_identity.type = "SystemAssigned" + if enable_managed_identity is None: + managed_service_identity = None + if assign_identity is not None: + raise CLIError("Cannot assign identity without enabling managed identity.") + elif enable_managed_identity is False: + managed_service_identity = fleet_managed_service_identity_model(type="None") + else: + managed_service_identity = fleet_managed_service_identity_model(type="SystemAssigned") if assign_identity is not None: + user_assigned_identity_model = cmd.get_models( + "UserAssignedIdentity", + resource_type=CUSTOM_MGMT_FLEET, + operation_group="fleets" + ) managed_service_identity.type = "UserAssigned" - managed_service_identity.user_assigned_identities = {assign_identity: None} + managed_service_identity.user_assigned_identities = {assign_identity: user_assigned_identity_model()} fleet_patch = fleet_patch_model( tags=tags, identity=managed_service_identity ) - fleet_patch = fleet_patch_model(tags=tags) - return sdk_no_wait(no_wait, client.begin_update, resource_group_name, name, fleet_patch) + return sdk_no_wait(no_wait, client.begin_update, resource_group_name, name, fleet_patch, polling_interval=5) def show_fleet(cmd, # pylint: disable=unused-argument @@ -164,7 +186,7 @@ def delete_fleet(cmd, # pylint: disable=unused-argument resource_group_name, name, no_wait=False): - return sdk_no_wait(no_wait, client.begin_delete, resource_group_name, name) + return sdk_no_wait(no_wait, client.begin_delete, resource_group_name, name, polling_interval=5) def get_credentials(cmd, # pylint: disable=unused-argument @@ -251,17 +273,17 @@ def create_update_run(cmd, fleet_name, name, upgrade_type, - node_image_selection, + node_image_selection=None, kubernetes_version=None, stages=None, - update_strategy_id=None, + update_strategy_name=None, no_wait=False): if upgrade_type == "Full" and kubernetes_version is None: raise CLIError("Please set kubernetes version when upgrade type is 'Full'.") if upgrade_type == "NodeImageOnly" and kubernetes_version is not None: raise CLIError("Cannot set kubernetes version when upgrade type is 'NodeImageOnly'.") - if stages is not None and update_strategy_id is not None: - raise CLIError("Cannot set stages when update strategy id is set.") + if stages is not None and update_strategy_name is not None: + raise CLIError("Cannot set stages when update strategy name is set.") update_run_strategy = get_update_run_strategy(cmd, "update_runs", stages) @@ -288,18 +310,36 @@ def create_update_run(cmd, managed_cluster_upgrade_spec = managed_cluster_upgrade_spec_model( type=upgrade_type, kubernetes_version=kubernetes_version) + if node_image_selection is None: + node_image_selection = "Latest" node_image_selection_type = node_image_selection_model(type=node_image_selection) managed_cluster_update = managed_cluster_update_model( upgrade=managed_cluster_upgrade_spec, node_image_selection=node_image_selection_type) + updateStrategyId = None + if update_strategy_name is not None: + subId = get_subscription_id(cmd.cli_ctx) + updateStrategyId = ( + f"/subscriptions/{subId}/resourceGroups/{resource_group_name}" + f"/providers/Microsoft.ContainerService/fleets/{fleet_name}/updateStrategies/{update_strategy_name}" + ) + update_run = update_run_model( - update_strategy_id=update_strategy_id, + update_strategy_id=updateStrategyId, strategy=update_run_strategy, managed_cluster_update=managed_cluster_update) - return sdk_no_wait(no_wait, client.begin_create_or_update, resource_group_name, fleet_name, name, update_run) + result = None + try: + result = sdk_no_wait(no_wait, client.begin_create_or_update, resource_group_name, fleet_name, name, update_run) + print("After successfully creating the run, you need to use the following command to start the run:" + f"az fleet updaterun start --resource-group={resource_group_name} --fleet={fleet_name} --name={name}") + except Exception as e: + return e + + return result def show_update_run(cmd, # pylint: disable=unused-argument diff --git a/src/fleet/azext_fleet/tests/latest/test_fleet_hubful_scenario.py b/src/fleet/azext_fleet/tests/latest/test_fleet_hubful_scenario.py index 49697a97046..6029811a9df 100644 --- a/src/fleet/azext_fleet/tests/latest/test_fleet_hubful_scenario.py +++ b/src/fleet/azext_fleet/tests/latest/test_fleet_hubful_scenario.py @@ -76,6 +76,6 @@ def test_fleet_hubful(self): self.cmd('fleet member wait -g {rg} --fleet-name {fleet_name} --fleet-member-name {member_name} --created', checks=[self.is_empty()]) - self.cmd('fleet member delete -g {rg} --fleet-name {fleet_name} -n {member_name}') + self.cmd('fleet member delete -g {rg} --fleet-name {fleet_name} -n {member_name} --yes') - self.cmd('fleet delete -g {rg} -n {fleet_name}') + self.cmd('fleet delete -g {rg} -n {fleet_name} --yes') diff --git a/src/fleet/azext_fleet/tests/latest/test_fleet_hubless_scenario.py b/src/fleet/azext_fleet/tests/latest/test_fleet_hubless_scenario.py index 0f85e0efe99..c089e307361 100644 --- a/src/fleet/azext_fleet/tests/latest/test_fleet_hubless_scenario.py +++ b/src/fleet/azext_fleet/tests/latest/test_fleet_hubless_scenario.py @@ -107,9 +107,9 @@ def test_fleet_hubless(self): self.check('name', '{updaterun}') ]) - self.cmd('fleet updaterun delete -g {rg} -n {updaterun} -f {fleet_name}') + self.cmd('fleet updaterun delete -g {rg} -n {updaterun} -f {fleet_name} --yes') - update_strategy_id = self.cmd('fleet updatestrategy create -g {rg} -n {updateStrategy_name} -f {fleet_name} --stages {stages_file}', checks=[ + update_strategy_name = self.cmd('fleet updatestrategy create -g {rg} -n {updateStrategy_name} -f {fleet_name} --stages {stages_file}', checks=[ self.check('name', '{updateStrategy_name}') ]).get_output_in_json()['id'] @@ -122,10 +122,10 @@ def test_fleet_hubless(self): ]) self.kwargs.update({ - 'update_strategy_id': update_strategy_id, + 'update_strategy_name': update_strategy_name, }) - self.cmd('fleet updaterun create -g {rg} -n {updaterun} -f {fleet_name} --upgrade-type Full --node-image-selection Latest --kubernetes-version 1.27.1 --update-strategy-id {update_strategy_id}', checks=[ + self.cmd('fleet updaterun create -g {rg} -n {updaterun} -f {fleet_name} --upgrade-type Full --node-image-selection Latest --kubernetes-version 1.27.1 --update-strategy-name {update_strategy_name}', checks=[ self.check('name', '{updaterun}') ]) @@ -141,10 +141,10 @@ def test_fleet_hubless(self): self.check('length([])', 1) ]) - self.cmd('fleet updaterun delete -g {rg} -n {updaterun} -f {fleet_name}') + self.cmd('fleet updaterun delete -g {rg} -n {updaterun} -f {fleet_name} --yes') - self.cmd('fleet updatestrategy delete -g {rg} -f {fleet_name} -n {updateStrategy_name}') + self.cmd('fleet updatestrategy delete -g {rg} -f {fleet_name} -n {updateStrategy_name} --yes') - self.cmd('fleet member delete -g {rg} --fleet-name {fleet_name} -n {member_name}') + self.cmd('fleet member delete -g {rg} --fleet-name {fleet_name} -n {member_name} --yes') - self.cmd('fleet delete -g {rg} -n {fleet_name}') + self.cmd('fleet delete -g {rg} -n {fleet_name} --yes') diff --git a/src/fleet/azext_fleet/tests/latest/test_validators.py b/src/fleet/azext_fleet/tests/latest/test_validators.py index d5f362c8f8c..0532441400d 100644 --- a/src/fleet/azext_fleet/tests/latest/test_validators.py +++ b/src/fleet/azext_fleet/tests/latest/test_validators.py @@ -39,8 +39,8 @@ def __init__(self, node_image_selection): class UpdateStrategyNamespace: - def __init__(self, update_strategy_id): - self.update_strategy_id = update_strategy_id + def __init__(self, update_strategy_name): + self.update_strategy_name = update_strategy_name class TestValidateMemberClusterId(unittest.TestCase): def test_invalid_member_cluster_id(self): @@ -144,21 +144,21 @@ def test_empty_identity_id(self): self.assertIsNone(validators.validate_assign_identity(namespace)) -class TestValidateUpdateStrategyId(unittest.TestCase): - def test_invalid_update_strategy_id(self): - invalid_update_strategy_id = "dummy cluster id" - namespace = UpdateStrategyNamespace(update_strategy_id=invalid_update_strategy_id) - err = ("--update-strategy-id is not a valid Azure resource ID.") +class TestValidateUpdateStrategyName(unittest.TestCase): + def test_invalid_update_strategy_name(self): + invalid_update_strategy_name = "" + namespace = UpdateStrategyNamespace(update_strategy_name=invalid_update_strategy_name) + err = ("--update-strategy-name is not a valid name") with self.assertRaises(CLIError) as cm: - validators.validate_update_strategy_id(namespace) + validators.validate_update_strategy_name(namespace) self.assertEqual(str(cm.exception), err) - def test_valid_update_strategy_id(self): - valid_update_strategy_id = "/subscriptions/123/resourceGroups/abc/providers/Microsoft.ContainerService/fleets/fleet-1/updateStrategies/strategy-1" - namespace = UpdateStrategyNamespace(update_strategy_id=valid_update_strategy_id) + def test_valid_update_strategy_name(self): + valid_update_strategy_name = "strategyname" + namespace = UpdateStrategyNamespace(update_strategy_name=valid_update_strategy_name) - self.assertIsNone(validators.validate_update_strategy_id(namespace)) + self.assertIsNone(validators.validate_update_strategy_name(namespace)) if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/src/fleet/setup.py b/src/fleet/setup.py index 2362e9d70cd..b4f83e679cd 100644 --- a/src/fleet/setup.py +++ b/src/fleet/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.2.7' +VERSION = '0.3.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers