Skip to content

Commit 2db3dde

Browse files
authored
[AKS] Support use custom kubelet identity (Azure#18615)
* Add custom kubelet identity implementation * Add recording file * Fix lint * Fix lint 2 * Apply code review * Fix errors * Apply code review * Fix lint * Fix lint 2
1 parent aaffbc5 commit 2db3dde

File tree

8 files changed

+1592
-11
lines changed

8 files changed

+1592
-11
lines changed

linter_exclusions.yml

+3
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ aks create:
260260
rule_exclusions:
261261
- option_length_too_long
262262
enable_encryption_at_host:
263+
rule_exclusions:
264+
- option_length_too_long
265+
assign_kubelet_identity:
263266
rule_exclusions:
264267
- option_length_too_long
265268
aks enable-addons:

src/azure-cli/azure/cli/command_modules/acs/_consts.py

+3
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@
4949
}
5050

5151
CONST_CANIPULL_IMAGE = "mcr.microsoft.com/aks/canipull:0.0.2-alpha"
52+
53+
CONST_MANAGED_IDENTITY_OPERATOR_ROLE = 'Managed Identity Operator'
54+
CONST_MANAGED_IDENTITY_OPERATOR_ROLE_ID = 'f1a07417-d97a-45cb-824c-7a7467783830'

src/azure-cli/azure/cli/command_modules/acs/_help.py

+5
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,9 @@
413413
- name: --assign-identity
414414
type: string
415415
short-summary: Specify an existing user assigned identity for control plane's usage in order to manage cluster resource group.
416+
- name: --assign-kubelet-identity
417+
type: string
418+
short-summary: Specify an existing user assigned identity for kubelet's usage, which is typically used to pull image from ACR.
416419
- name: --node-osdisk-diskencryptionset-id -d
417420
type: string
418421
short-summary: ResourceId of the disk encryption set to use for enabling encryption at rest on agent node os disk.
@@ -489,6 +492,8 @@
489492
text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-ultra-ssd
490493
- name: Create a kubernetes cluster with Azure RBAC enabled.
491494
text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-aad --enable-azure-rbac
495+
- name: Create a kubernetes cluster with custom control plane identity and kubelet identity.
496+
text: az aks create -g MyResourceGroup -n MyManagedCluster --assign-identity <control-plane-identity-resource-id> --assign-kubelet-identity <kubelet-identity-resource-id>
492497
"""
493498

494499
helps['aks update'] = """

src/azure-cli/azure/cli/command_modules/acs/_params.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
validate_priority, validate_eviction_policy, validate_spot_max_price,
2525
validate_load_balancer_outbound_ip_prefixes, validate_taints, validate_ip_ranges, validate_acr, validate_nodepool_tags,
2626
validate_load_balancer_outbound_ports, validate_load_balancer_idle_timeout, validate_vnet_subnet_id, validate_nodepool_labels,
27-
validate_ppg, validate_assign_identity, validate_max_surge)
27+
validate_ppg, validate_assign_identity, validate_max_surge, validate_assign_kubelet_identity)
2828
from ._consts import CONST_OUTBOUND_TYPE_LOAD_BALANCER, CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, \
2929
CONST_SCALE_SET_PRIORITY_REGULAR, CONST_SCALE_SET_PRIORITY_SPOT, \
3030
CONST_SPOT_EVICTION_POLICY_DELETE, CONST_SPOT_EVICTION_POLICY_DEALLOCATE, \
@@ -264,6 +264,7 @@ def load_arguments(self, _):
264264
'--appgw-subnet-id'], arg_group='Application Gateway')
265265
c.argument('appgw_watch_namespace', options_list=[
266266
'--appgw-watch-namespace'], arg_group='Application Gateway')
267+
c.argument('assign_kubelet_identity', validator=validate_assign_kubelet_identity)
267268
c.argument('yes', options_list=[
268269
'--yes', '-y'], help='Do not prompt for confirmation.', action='store_true')
269270
c.argument('enable_sgxquotehelper', action='store_true')

src/azure-cli/azure/cli/command_modules/acs/_validators.py

+9
Original file line numberDiff line numberDiff line change
@@ -421,3 +421,12 @@ def validate_assign_identity(namespace):
421421
from msrestazure.tools import is_valid_resource_id
422422
if not is_valid_resource_id(namespace.assign_identity):
423423
raise InvalidArgumentValueError("--assign-identity is not a valid Azure resource ID.")
424+
425+
426+
def validate_assign_kubelet_identity(namespace):
427+
if namespace.assign_kubelet_identity is not None:
428+
if namespace.assign_kubelet_identity == '':
429+
return
430+
from msrestazure.tools import is_valid_resource_id
431+
if not is_valid_resource_id(namespace.assign_kubelet_identity):
432+
raise InvalidArgumentValueError("--assign-kubelet-identity is not a valid Azure resource ID.")

src/azure-cli/azure/cli/command_modules/acs/custom.py

+68-9
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@
4747
from azure.cli.command_modules.acs._params import regions_in_preview, regions_in_prod
4848
from azure.cli.core.api import get_config_dir
4949
from azure.cli.core.azclierror import (ResourceNotFoundError,
50-
ArgumentUsageError,
5150
ClientRequestError,
51+
ArgumentUsageError,
5252
InvalidArgumentValueError,
5353
MutuallyExclusiveArgumentError,
54-
ValidationError)
54+
ValidationError,
55+
UnauthorizedError)
5556
from azure.cli.core._profile import Profile
5657
from azure.cli.core.profiles import ResourceType
5758
from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id
@@ -100,6 +101,7 @@
100101
from ._consts import ADDONS
101102
from ._consts import CONST_CANIPULL_IMAGE
102103
from ._consts import CONST_PRIVATE_DNS_ZONE_SYSTEM
104+
from ._consts import CONST_MANAGED_IDENTITY_OPERATOR_ROLE, CONST_MANAGED_IDENTITY_OPERATOR_ROLE_ID
103105

104106
logger = get_logger(__name__)
105107

@@ -784,10 +786,16 @@ def _generate_properties(api_version, orchestrator_type, orchestrator_version, m
784786
return properties
785787

786788

787-
def _get_user_assigned_identity_client_id(cli_ctx, resource_id):
788-
pattern = '/subscriptions/(.*?)/resourcegroups/(.*?)/providers/microsoft.managedidentity/userassignedidentities/(.*)' # pylint: disable=line-too-long
789+
def _get_user_assigned_identity_resource_id_regular_expression():
790+
return re.compile(
791+
r'/subscriptions/(.*?)/resourcegroups/(.*?)/providers/microsoft.managedidentity/userassignedidentities/(.*)',
792+
flags=re.IGNORECASE)
793+
794+
795+
def _get_user_assigned_identity(cli_ctx, resource_id):
789796
resource_id = resource_id.lower()
790-
match = re.search(pattern, resource_id)
797+
_re_user_assigned_identity_resource_id = _get_user_assigned_identity_resource_id_regular_expression()
798+
match = _re_user_assigned_identity_resource_id.search(resource_id)
791799
if match:
792800
subscription_id = match.group(1)
793801
resource_group_name = match.group(2)
@@ -798,14 +806,21 @@ def _get_user_assigned_identity_client_id(cli_ctx, resource_id):
798806
resource_name=identity_name)
799807
except CloudError as ex:
800808
if 'was not found' in ex.message:
801-
raise ResourceNotFoundError(
802-
"Identity {} not found.".format(resource_id))
809+
raise ResourceNotFoundError("Identity {} not found.".format(resource_id))
803810
raise ClientRequestError(ex.message)
804-
return identity.client_id
811+
return identity
805812
raise InvalidArgumentValueError(
806813
"Cannot parse identity name from provided resource id {}.".format(resource_id))
807814

808815

816+
def _get_user_assigned_identity_client_id(cli_ctx, resource_id):
817+
return _get_user_assigned_identity(cli_ctx, resource_id).client_id
818+
819+
820+
def _get_user_assigned_identity_object_id(cli_ctx, resource_id):
821+
return _get_user_assigned_identity(cli_ctx, resource_id).principal_id
822+
823+
809824
# pylint: disable=too-many-locals
810825
def acs_create(cmd, client, resource_group_name, deployment_name, name, ssh_key_value, dns_name_prefix=None,
811826
location=None, admin_username="azureuser", api_version=None, master_profile=None,
@@ -1958,6 +1973,7 @@ def aks_create(cmd, client, resource_group_name, name, ssh_key_value, # pylint:
19581973
appgw_watch_namespace=None,
19591974
enable_sgxquotehelper=False,
19601975
enable_encryption_at_host=False,
1976+
assign_kubelet_identity=None,
19611977
enable_ultra_ssd=False,
19621978
no_wait=False,
19631979
yes=False,
@@ -1992,6 +2008,9 @@ def aks_create(cmd, client, resource_group_name, name, ssh_key_value, # pylint:
19922008
ManagedClusterIdentity = cmd.get_models('ManagedClusterIdentity',
19932009
resource_type=ResourceType.MGMT_CONTAINERSERVICE,
19942010
operation_group='managed_clusters')
2011+
ManagedClusterPropertiesIdentityProfileValue = cmd.get_models('ManagedClusterPropertiesIdentityProfileValue',
2012+
resource_type=ResourceType.MGMT_CONTAINERSERVICE,
2013+
operation_group='managed_clusters')
19952014
ManagedCluster = cmd.get_models('ManagedCluster',
19962015
resource_type=ResourceType.MGMT_CONTAINERSERVICE,
19972016
operation_group='managed_clusters')
@@ -2291,6 +2310,25 @@ def aks_create(cmd, client, resource_group_name, name, ssh_key_value, # pylint:
22912310
user_assigned_identities=user_assigned_identity
22922311
)
22932312

2313+
identity_profile = None
2314+
if assign_kubelet_identity:
2315+
if not assign_identity:
2316+
# pylint: disable=line-too-long
2317+
raise ArgumentUsageError('--assign-kubelet-identity can only be specified when --assign-identity is specified')
2318+
kubelet_identity = _get_user_assigned_identity(cmd.cli_ctx, assign_kubelet_identity)
2319+
identity_profile = {
2320+
'kubeletidentity': ManagedClusterPropertiesIdentityProfileValue(
2321+
resource_id=assign_kubelet_identity,
2322+
client_id=kubelet_identity.client_id,
2323+
object_id=kubelet_identity.principal_id
2324+
)
2325+
}
2326+
cluster_identity_object_id = _get_user_assigned_identity_object_id(cmd.cli_ctx, assign_identity)
2327+
# ensure the cluster identity has "Managed Identity Operator" role at the scope of kubelet identity
2328+
_ensure_cluster_identity_permission_on_kubelet_identity(
2329+
cmd.cli_ctx,
2330+
cluster_identity_object_id)
2331+
22942332
mc = ManagedCluster(
22952333
location=location,
22962334
tags=tags,
@@ -2307,7 +2345,8 @@ def aks_create(cmd, client, resource_group_name, name, ssh_key_value, # pylint:
23072345
auto_scaler_profile=cluster_autoscaler_profile,
23082346
api_server_access_profile=api_server_access_profile,
23092347
identity=identity,
2310-
disk_encryption_set_id=node_osdisk_diskencryptionset_id
2348+
disk_encryption_set_id=node_osdisk_diskencryptionset_id,
2349+
identity_profile=identity_profile
23112350
)
23122351

23132352
use_custom_private_dns_zone = False
@@ -4694,3 +4733,23 @@ def _put_managed_cluster_ensuring_permission(
46944733
headers=headers)
46954734

46964735
return cluster
4736+
4737+
4738+
def _ensure_cluster_identity_permission_on_kubelet_identity(cli_ctx, cluster_identity_object_id, scope):
4739+
factory = get_auth_management_client(cli_ctx, scope)
4740+
assignments_client = factory.role_assignments
4741+
4742+
for i in assignments_client.list_for_scope(scope=scope, filter='atScope()'):
4743+
if i.scope.lower() != scope.lower():
4744+
continue
4745+
if not i.role_definition_id.lower().endswith(CONST_MANAGED_IDENTITY_OPERATOR_ROLE_ID):
4746+
continue
4747+
if i.principal_id.lower() != cluster_identity_object_id.lower():
4748+
continue
4749+
# already assigned
4750+
return
4751+
4752+
if not _add_role_assignment(cli_ctx, CONST_MANAGED_IDENTITY_OPERATOR_ROLE, cluster_identity_object_id,
4753+
is_service_principal=False, scope=scope):
4754+
raise UnauthorizedError('Could not grant Managed Identity Operator '
4755+
'permission to cluster identity at scope {}'.format(scope))

0 commit comments

Comments
 (0)